Integrimi në stilin BPM

Integrimi në stilin BPM

Përshendetje, Habr!

Kompania jonë specializohet në zhvillimin e zgjidhjeve softuerike të klasës ERP, ku një pjesë e madhe zënë sistemet transaksionale me një volum të madh logjike biznesi dhe rrjedhës së dokumentacionit të ngjashme me SED. Versionet moderne të produkteve tona bazohen në teknologjite JavaEE, por ne gjithashtu eksperimentojmë aktivisht me mikroshërbimet. Një nga vendet më problematike të këtij lloji zgjidhjesh është integrimi i sub-sistemeve të ndryshme që i përkasin domain-eve lidhëse. Detyrat e integrimit gjithmonë na kanë shkaktuar një dhimbje të madhe koke, pavarësisht nga stillet arkitekturore, staketet teknologjike dhe kuadrot që kemi aplikuar. Megjithatë, së fundmi ka pasur progres në zgjidhjen e këtyre problemeve.

Në artikullin që ju paraqesim, do të flas për përvojën dhe eksperimentet arkitektonike të NPO "Krista" në këtë fushë. Gjithashtu do të shqyrtojmë një shembull të një zgjidhjeje të thjeshtë për një problem integrimi nga perspektiva e zhvilluesit aplikativ dhe do të zbulojmë se çfarë fshihet pas kësaj thjeshtësie.

Kufizimi

Zgjidhjet arkitektonike dhe teknike të përshkruara në artikull ofrohen nga unë mbi bazën e eksperiencës personale në kontekstin e detyrave të caktuara. Këto zgjidhje nuk pretendojnë të jenë universale dhe mund të mos jenë optimale në kushte të tjera përdorimi.

Çfarë lidhje ka BPM me këtë?

Për të përgjigjur këtë pyetje, është e nevojshme të thellohemi pak në specifikën e detyrave praktike të zgjidhjeve tona. Pjesa kryesore e logjikës së biznesit në sistemin tonë tipik transaksional përfshin hyrjen e të dhënave në BDB përmes ndërfaqeve të përdoruesit, kontrollin manual dhe automatizuar të këtyre të dhënave, kalimin e tyre në disa workflow, publikimin në një sistem tjetër / bazë analitike / arkiv, dhe hartimin e raporteve. Kështu, funksioni kyç i sistemit për klientët është automatizimi i proceseve të tyre të brendshme të biznesit.

Për lehtësinë tonë, ne përdorim në komunikim termin "dokumenti" si një abstraksion të një grupi të dhënash të bashkuara me një çelës të zakonshëm, të cilit mund t'i "ngjitim" një workflow të caktuar.
Por si si të integroni logjikën e integrimit? Sepse detyra e integrimit lind nga arkitektura e sistemit, i cili është "ndarë" në pjesë JO sipas kërkesës së klientit, por për shkak të faktorëve të tjerë.

  • në përputhje me ligjin e Conway-t;
  • si rezultat i ripërdorimit të nën-sistemeve, të cilat janë zhvilluar më parë për produkte të tjera;
  • sipërme nga vendimi i arkitektit, në përputhje me kërkesat jo funksionale.

Ekziston një joshje e madhe për të ndarë logjikën e integrimit nga logjika e biznesit të punës kryesore, për të mos e ndotur logjikën e biznesit me artefakte integruese dhe për të shpëtuar zhvilluesin aplikativ nga nevoja për t'u angazhuar me veçoritë e peizazhit arkitektonik të sistemit. Ky qasje ka disa përparësi, megjithatë praktika tregon për efikasitetin e saj të ulët:

  • zgjidhja e problemeve të integrimit zakonisht kthehet në variantet më të thjeshta në formën e thirrjeve sinkrone për shkak të kufizimeve të pikave të zgjerimit në realizimin e punës kryesore (për disavantazhet e integrimit sinkron – pak më poshtë);
  • artefaktet integruese gjithsesi depërtojnë në logjikën kryesore të biznesit, kur kërkohet një feedback nga një nën-sistem tjetër;
  • një zhvillues aplikativ injoron integrimin dhe mund ta prishë lehtësisht duke ndryshuar procesin e punës;
  • sistemi ndalon së qeni një tërësi në pikëpamjen e përdoruesit, bëhen të dukshme «gulfat» mes nënsistemeve, shfaqen operacione të tepërta nga ana e përdoruesit që iniciativojnë transferimin e të dhënave nga një nënsiistem në tjetrin.

Një qasje tjetër është të shohësh ndërveprimet integruese si një pjesë të pandashme të logjikës kryesore të biznesit dhe rrjedhës së punës. Për të parandaluar që kërkesat e kualifikimeve të zhvilluesve të aplikacioneve të rriten në nivele të papërballueshme, krijimi i ndërveprimeve të reja integruese duhet të bëhet lehtësisht dhe pa ndjenja të detyrueshme, me mundësi minimale për të zgjedhur mënyrën e zgjidhjes. Kjo është më e komplikuar se sa duket: mjeti duhet të jetë mjaft i fuqishëm për të ofruar përdoruesit një shumëllojshmëri opsionesh për ta aplikuar atë, duke mos lejuar që ata të bëjnë gabime serioze. Ka shumë pyetje që inxhinieri duhet të përgjigjet në kontekstin e detyrave integruese, por për të cilat një zhvillues aplikacionesh 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, rrugëzimi, marshalling, përhapja dhe kalimi i konteksteve etj. Duhet të ofrohen zhvilluesve të aplikacioneve modele zgjidhjesh mjaft të thjeshta, në të cilat janë fshehur tashmë përgjigjet për të gjitha këto pyetje. Këto modele duhet të jenë mjaft të sigurta: logjika e biznesit ndryshon shpesh, gjë që rrit rrezikun e gabimeve dhe çmimi i gabimeve duhet të mbetet në nivele të ulëta.

Por mirësinë e rastit, çfarë lidhje ka BPM me këtë? Ekzistojnë shumë varianta për zbatimin e workflow…
Në të vërtetë, në zgjidhjet tona një implementim tjetër i biznes-procesëve është jashtëzakonisht popullor – përmes përcaktimit deklarativ të diagramës së kalimeve të gjendjeve dhe lidhjes së trajtuesve me logjikën e biznesit për kalimet. Në këtë rast, gjendja që përcakton pozicionin aktual të "dokuments" në biznes-proces është një atribut i vetë "dokuments".

Integrimi në stilin BPM
Ja si duket procesi në fillimin e projektit

Popullariteti i këtij implementimi është i justifikuar nga relativisht thjeshtësia dhe shpejtësia e krijimit të biznes-procesëve linearë. Megjithatë, me rritjen e vazhdueshme të kompleksitetit të sistemeve software, pjesa automatike e biznes-procesit zgjeron dhe komplikohet. Shfaqet nevoja për dekompozim, ripërdorim të pjesëve të proceseve, si dhe për degëzim të proceseve, në mënyrë që secila degë të ekzekutohet paralelisht. Në këto kushte, mjeti bëhet i papërshtatshëm, dhe diagramat e kalimeve të gjendjeve humbin informativitetin (interaksionet integruese nuk reflektohen fare në diagram).

Integrimi në stilin BPM
Ky është procesi pas disa iteracioneve të sqarimit të kërkesave

Zgjidhja për këtë situatë ishte integrimi i motorit jBPM në disa produkte me proceset më komplekse të biznesit. Në horizontin afatshkurtër, ky zgjidhje pati disa sukses: u krijua mundësia për realizimin e proceseve komplekse të biznesit me një diagramë mjaft informative dhe aktuale në notacionin BPMN2.

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

Në perspektivën afatgjatë, zgjidhja nuk i përmbushi pritjet: ecuria e madhe e krijimit të proceseve të biznesit përmes mjeteve vizuale nuk lejojti arritjen e treguesve të pranueshëm të produktivitetit, dhe vetë mjetet u bënë një nga më të padëshiruara mes zhvilluesve. Kishin gjithashtu shqetësime për strukturën e brendshme të motorit, gjë që çoi në krijimin e shumë "patch-e" dhe "përforcimeve".

Një nga përfitimet kryesore të përdorimit të jBPM ishte njohja e dobisë dhe dëmshmërisë së praniës së një gjendjeje të qëndrueshme në instancën e procesit të biznesit. Gjithashtu, ne pamë mundësinë e aplikimit të qasjes procesore për të realizuar protokolle të ndërlikuara integrimi midis aplikacioneve të ndryshme duke përdorur ndërveprime asinkrone përmes sinjaleve dhe mesazheve. Prania e një gjendjeje të qëndrueshme luan një rol të rëndësishëm në këtë.

Për bazë të asaj që u tha, mund të përfundohet: qasja procesore në stilin BPM na lejon të zgjidhim një gamë të gjerë detyrash për automatizimin e proceseve të biznesit që bëhen gjithnjë e më të ndërlikuara, për të integruar aktivitetet dhe për të ruajtur mundësinë e vizualizimit të procesit të realizuar në një notacion të përshtatshëm për këtë.

Disavantazhet e thirrjeve sinkrone si një model integrimi

Mehulli i integrimit nënkupton një thirrje të thjeshtë bllokuese. Një nën-sistem vepron si ana server dhe ekspozon një API me metodën e nevojshme. Ana tjetër vepron si ana klient dhe në momentin e duhur bën thirrjen duke pritur rezultatin. Sipas arkitekturës së sistemit, ana klient dhe ana server mund të vendosen ose në një aplikacion dhe proces të vetëm, ose në të ndryshme. Në rastin e dytë, nevojitet të aplikohet një implementim i caktuar RPC dhe të sigurohet marshallimi i parametrave dhe rezultatit të thirrjes.

Integrimi në stilin BPM

Ky model integrimi ka një set të konsiderueshëm disavantazhesh, por është shumë i përdorur praktike për shkak të thjeshtësisë së tij. Shpejtësia e implementimit është tërheqëse dhe e detyron atë të përdoret përsëri e përsëri në kushte 'urgjente', duke e regjistruar zgjidhjen si borxhin teknik. Por ndonjëherë ndodh që zhvilluesit e panjohur ta përdorin atë pa një kuptim të vetëdijshëm, thjesht pa e ditur për pasojat negative.

Përveç përmirësimit më të dukshëm të lidhjes së nën-sistemeve, ekzistojnë edhe probleme më pak të dukshme me "shkëputjen" 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 e tyre, bllokojnë resurset specifike të aplikacionit që preken nga këto ndryshime. Kështu që derisa një nën-sistem të presë një përgjigje nga një tjetër, ai nuk mund të përfundojë transaksionin dhe të heqë bllokimet. Kjo ndjeshëm rrit rrezikun e shfaqjes së efekteve të ndryshme:

  • mungesën e përgjigjes së sistemit, përdoruesit presin për një kohë të gjatë përgjigje për kërkesat;
  • serveri ndalon së përgjigjuri për kërkesat e përdoruesve për shkak të mbushjes së rezervuarit të fijeve: shumica e fijeve "kanë ngecur" në bllokimin e një burimi, i zënë nga një transaksion;
  • fillon të shfaqen ddeadlocks: 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 gabime për tejkalimin e kohës së transaksionit;
  • Serveri «bjerë» për shkak të OutOfMemory nëse detyra kërkon përpunimin dhe modifikimin e sasi të mëdha të të dhënave, ndërsa prania e integrimeve sinkrone e vështirëson shumë ndarjen e përpunimit në transaksione «më të lehta».

Nga një këndvështrim arkitektural, përdorimi i thirrjeve bllokuese gjatë integrimit çon në humbjen e kontrollit mbi cilësinë e nën-sistemeve të veçanta: nuk është e mundur të sigurohen treguesin e cilësisë të një nën-sistemi në mënyrë të veçuar nga treguesit e cilësisë së nën-sistemit tjetër. Nëse nën-sistemet zhvillohen nga ekipe të ndryshme, kjo paraqet një problem të madh.

Të gjitha bëhen edhe më interesante nëse nën-sistemet e integruara ndodhen në aplikacione të ndryshme dhe është e nevojshme të bëhen ndryshime sinkrone nga të dyja anët. Si mund të sigurohet transaksionaliteti i këtyre ndryshimeve?

Nëse ndryshimet bëhen në transaksione të ndara, atëherë do të kërkohet të sigurohet përpunim të besueshëm të përjashtimeve dhe kompensimeve, dhe kjo do të neutralizojë plotësisht përparësinë kryesore të integrimeve sinkrone – lehtësinë.

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

«Saga» si zgjidhje për problemet e transaksioneve

Me rritjen e popullaritetit të mikroservicëve, kërkesa për Modelin Saga.

Ky model zgjidh përmirësisht problemet e përmendura më sipër të transaksioneve të gjata dhe gjithashtu zgjeron mundësitë e menaxhimit të qëndrueshmërisë së sistemit nga ana e logjikës biznesore: kompensimi pas një transaksioni të dështuar nuk mund ta rikthejë sistemin në gjendjen e tij fillestare, por mund të sigurojë një rrugë alternative për trajtimin e të dhënave. Kjo gjithashtu lejon që të mos përsëriten hapat e përpunimit të dhënave që kanë përfunduar me sukses gjatë përpjekjeve të përsëritura për ta çuar procesin në një përfundim 'të mirë'.

Ajo që është interesante, në sistemet monolitike ky model është gjithashtu i rëndësishëm, nëse flasim për integrimin e nën-sistemeve të lidhura dobët dhe ndihen 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 «Saga» tregon të jetë shumë i lehtë: hapat e veçanta të «Saga» mund të përcaktohen si aktivitete brenda procesit të biznesit, ndërsa gjendja e qëndrueshme e procesit të biznesit përcakton edhe gjendjen e brendshme të «Saga». Kështu që nuk kemi nevojë për ndonjë mekanizëm koordinues shtesë. Do të nevojitej vetëm një broker mesazhesh me mbështetje për garantimin «të paktën një herë» si transport.

Por ky zgjidhje ka edhe çmimin e vet:

  • logjika e biznesit bëhet më e komplikuar: duhet që të trajtohen kompensimet;
  • duhet të heqim dorë nga konsistenca e plotë, që mund të jetë veçanërisht e ndjeshme për sistemet monolite;
  • arkitektura paksa e komplikon, lind një nevojë shtesë për një broker mesazhesh;
  • do të nevojiten mjete shtesë për monitorimin dhe administrimin (ndonëse në përgjithësi kjo madje është e mirë: cilësia e shërbimit të sistemit do të rritet).

Për sistemet monolitike, kjo qëllimshmëri e përdorimit të "Saga" nuk është aq e dukshme. Për mikroshërbimet dhe SOA-të e 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.

Inkuadrimi i logjikës biznesore në mikroshërbime

Kur filluam të eksperimentojmë me mikroshërbimet, doli një pyetje e drejtë: ku të vendosim logjikën e biznesit në lidhje me shërbimin që siguron persistencën e të dhënave të domainit?

Duke parë arkitekturën e BPMS-ve të ndryshme, mund të duket e arsyeshme të ndahet logjika e biznesit nga persitenca: të krijohet një shtresë e mikroshërbimeve të platformës dhe të pavarura nga domaini, që formojnë një ambient dhe kontenier për ekzekutimin e logjikës së biznesit domain, ndërsa persistencën e të dhënave të domainit ta rregullojmë me një shtresë të veçantë nga mikroshërbime shumë të thjeshta dhe të lehta. Proceset e biznesit në këto raste kryejnë orkestrimin e shërbimeve të shtresës së persistencës.

Integrimi në stilin BPM

Ky lloj qasje ka një përparësi të madhe: mund të shtohet pa kufizim funksionaliteti i platformës, dhe "të mbushet" nga kjo do të jetë vetëm shtresa përkatëse e mikroshërbimeve të platformës. Proceset biznesore nga çdo fushë menjëherë fituan mundësinë për të përdorur funksionalitetin e ri të platformës, sa herë që ajo të përditësohet.

Një analizë më e detajuar zbuloi disavantazhe thelbësore të këtij qasjeje:

  • shërbimi i platformës, që ekzekuton logjikën e biznesit për shumë fusha, përmban rreziqe të mëdha si një pikë e vetme dështimi. Ndryshimet e shpeshta në logjikën e biznesit rrisin rrezikun e gabimeve që shkaktojnë dështime që përhapen në sistemin e gjithëpërfshirës;
  • probleme me performancën: logjika e biznesit punon me të dhënat e saj përmes një ndërfaqeje të ngushtë dhe të ngadalshëm:
    • të dhënat do të marshallohet për një herë të dytë dhe do të kalojnë nëpër strukturën e rrjetit;
    • shërbimi domen do të japë shpesh më shumë të dhëna se sa kërkohet për logjikën e biznesit për përpunim, për shkak të mundësive të pamjaftueshme për parametrizimin e kërkesave në nivelin e API-së së jashtme të shërbimit;
    • disa pjesë të pavarura të logjikës së biznesit mund të kërkojnë të njëjtat të dhëna për përpunim (kjo çështje mund të lehtësohet duke shtuar komponente sesioni që ruajnë të dhënat, por kjo e komplikon më tej arkitekturën dhe krijon probleme mbi aktualitetin e të dhënave dhe invalidimin e caches);
  • problemet e transaksionit:
    • proceset e biznesit me një gjendje persistente, ruajtja e së cilës është përgjegjësi e shërbimit të platformës, do të jenë disi të papajtueshme me të dhënat e domenit, dhe nuk parashikohet ndonjë zgjidhje e thjeshtë për këtë problem;
    • shkëputja e bllokimit të të dhënave të domenit jashtë transaksionit: nëse logjika e biznesit të domenit kërkon të bëjë ndryshime, duke verifikuar paraprakisht korrektësinë e të dhënave aktuale, duhet të sigurohet që të mos ketë mundësi për ndryshime konkurruese të të dhënave që po përpunohen. Bllokimi i jashtëm i të dhënave mund të ndihmojë për të zgjidhur problemin, por kjo zgjidhje sjell rreziqe të tilla dhe ul besueshmërinë e përgjithshme të sistemit;
  • kompleksitet shtesë gjatë përditësimit: në disa raste, duhet të përditësohet shërbimi i persistencës dhe logjika e biznesit në mënyrë sinkrone ose në një sekuencë strikte.

Në fund të fundit, duhej të kthehesha në burime: të inkapsuloj të dhënat e domenit dhe logjikën e biznesit të domenit brenda një mikroshërbimi. Ky qasje e thjeshton perceptimin e mikroshërbimit si një komponent tërësor në sistem dhe nuk shkakton problemet e mësipërme. Kjo gjithashtu nuk vjen pa kosto:

  • nevojitet standardizimi i API për ndërveprimin me logjikën e biznesit (veçanërisht për të siguruar aktivitetet e përdoruesve brenda proceseve të biznesit) dhe shërbimet e platformës API; kërkohet një vëmendje më e kujdesshme ndaj ndryshimeve të API, përputhshmërisë direkte dhe të kundërt;
  • nevojitet shtimi i bibliotekave shtesë në runtime për të siguruar funksionimin e logjikës së biznesit brenda çdo mikroshërbimi të tillë, dhe kjo gjeneron kërkesa të reja për këto biblioteka: lehtësinë dhe minimumin e varësive tranzitive;
  • Zhvilluesit e logjikës biznesore duhet të monitorojnë versionet e bibliotekeve: nëse ndonjë mikroshërbim nuk është përmirësuar për një kohë të gjatë, ka shumë mundësi që në të të jetë një version i vjetruar i bibliotekeve. Kjo mund të shkaktojë pengesa të papritura për shtimin e një funksionaliteti të ri dhe mund të kërkojë migrazhimin e logjikës së vjetër të biznesit të atij shërbimi në versione të reja bibliotekash, nëse midis versioneve ka pasur ndryshime të papajtueshme.

Integrimi në stilin BPM

Shtresa e shërbimeve platformë në një arkitekturë të tillë gjithashtu është e pranishme, por kjo shtresë nuk formon më një enë për ekzekutimin e logjikës biznesore domenale, por vetëm një mjedis për të, duke ofruar funksione ndihmëse 'platformë'. Kjo shtresë është e nevojshme jo vetëm për ruajtjen e lehtësisë së mikroshërbimeve domenale, por edhe për qendrim të menaxhimit.

Për shembull, aktivitetet e përdoruesve në proceset e biznesit krijojnë detyra. Megjithatë, duke punuar me detyra, përdoruesi duhet të shohë detyrat nga të gjithë domainët në një listë të Përgjithshme, prandaj duhet të ketë një shërbim përkatës të platformës për regjistrimin e detyrave, i pastruar nga logjika e biznesit të domainit. Ruajtja e inkapsulimit të logjikës së biznesit në një kontekst të tillë është mjaft problematike, dhe kjo është një kompromis tjetër i kësaj arkitekture.

Integrimi i proceseve të biznesit sipas zhvilluesit aplikativ

Siç u tha më parë, zhvilluesi aplikativ duhet të jetë i abstarhuar nga tiparet teknike dhe inxhinierike të implementimit të ndërveprimit të disa aplikacioneve, në mënyrë që të pritet një produktivitet i mirë i zhvillimit.

Le të përpiqemi të zgjidhim një detyrë integrimi relativisht të vështirë, të krijuar posaçërisht për këtë artikull. Kjo do të jetë një detyrë "lojë" që përfshin tre aplikacione, ku secili prej tyre përcakton një emër domaini: "app1", "app2", "app3".

Në çdo aplikacion, proceset biznesore fillojnë të "luajnë topin" përmes një buze integrimi. Mesazhet e quajtura "Topi" do të luajnë rolin e topit.

Rregullat e lojës:

  • lojtar i parë – iniciatori. Ai fton lojtarët e tjerë në lojë, fillon lojën dhe mund ta përfundojë atë në çdo moment;
  • lojtarët e tjerë shpallin pjesëmarrjen e tyre në lojë, "njoftohen" me njëri-tjetrin dhe me lojtarin e parë;
  • duke marrë topin, një lojtar zgjedh një lojtar tjetër që merr pjesë dhe i kalon topin. Numërohen gjithsej kalimet;
  • çdo lojtar ka "energi", e cila paksohet me çdo kalim të topit nga ky lojtar. Pas konsumimit të energjisë, lojtari përjashtohet nga loja, duke shpallur tërheqjen e tij;
  • nëse lojtari mbetet vetëm, ai menjëherë shpall tërheqjen e tij;
  • kur të gjithë lojtarët përjashtohen, lojtarin e parë shpall përfundimin e lojës. Nëse ai është përjashtuar më parë, ai mbetet për të mbikëqyrur lojën, për ta përfunduar atë.

Për të zgjidhur këtë detyrë, do të përdor DSL tonë për proceset biznesore, e cila lejon përshkrimin e logjikës në Kotlin në mënyrë kompakte, me minimumin e kodit të përsëritur.

Në aplikacionin app1 do të funksionojë processi biznesor i lojtarit të parë (ai që është iniciatori i lojës):

klasa InitialPlayer

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

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

class PlayersList : ArrayList()

// Ky është 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
}

// Kjo është deklarata e modelit të procesit: krijohet një herë, përdoret nga të gjithë
// instancat e procesit përkatës
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 biznesor
    val sendNewGameSignal = signal("NewGame")
    val sendStopGameSignal = signal("StopGame")
    val startTask = humanTask("Start") {
        taskOperation {
            processCondition { players.size > 0 }
            confirmation { "Kanë u lidhur ${players.size} lojtarë. Të fillojmë?" }
        }
    }
    val stopTask = humanTask("Stop") {
        taskOperation {}
    }
    val waitPlayerJoin = signalWait("PlayerJoin") { signal ->
        players.add(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... bashkangjitur lojtar ${signal.data} ...")
    }
    val waitPlayerOut = signalWait("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... lojtar ${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 konstruojmë grafikun e procesit nga aktivitetet e deklaruara
    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)
    }

    // Vendosim mbi aktivitetet trajtues të tjerë për regjistrimin
    sendNewGameSignal.onExit { println("Le të luajmë!") }
    sendStopGameSignal.onExit { println("Ndalo!") }
    sendPlayerOut.onExit { println("$playerName: Jam jashtë!") }
}

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 është në gjendje të ofrojë një model objekti të procesit biznesor, i cili mund të vizualizohet në formën e një diagrami. Aktualisht nuk e kemi realizuar vizualizuesin, prandaj pata nevojë të kaloj pak kohë në vizatim (këtu kam thjeshtuar pak notacionin BPMN në pjesën e përdorimit të gate-ve, për të përmirësuar koherencën e diagramit me kodin e paraqitur):

Integrimi në stilin BPM

Aplikacioni app2 do të përfshijë procesin biznesor të një lojtari 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ë aplikacionin app3 do ta bëjmë lojtarin të veprojë me një sjellje 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()

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

Në pjesën tjetër, sjellja e lojtarit nuk ndryshon nga e mëparshmja, kështu që diagrami nuk ndryshon.

Tani nevojitet një test për ta ekzekutuar të gjithë këtë. Do të jap vetëm kodin e vetë testit për të mos e mbingarkuar artikullin me bojlerplate (në të vërtetë kam përdorur një ambient testimi të krijuar më parë për testimin e integrimit të 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, që lojtarët të "njoftohen" midis tyre.
    // Të presësh përmes sleep është një zgjidhje e keqe, megjithatë është më e thjeshtë.
    // Mos e bëni këtë në teste serioze!
    Thread.sleep(1000);
    // Nisni 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;
}

Nisemi testin, shikojmë logun:

dalja në konzol

Взята блокировка ключа lock://app1/process/InitialPlayer
Let's play!
Снята блокировка ключа lock://app1/process/InitialPlayer
Player2: I'm here!
Player3: I'm here!
Player4: I'm here!
Player5: I'm here!
... join player Player2 ...
... join player Player4 ...
... join player Player3 ...
... join player Player5 ...
Step 1: Player1 >>> Player3
Step 2: Player3 >>> Player5
Step 3: Player5 >>> Player3
Step 4: Player3 >>> Player4
Step 5: Player4 >>> Player3
Step 6: Player3 >>> Player4
Step 7: Player4 >>> Player5
Step 8: Player5 >>> Player2
Step 9: Player2 >>> Player5
Step 10: Player5 >>> Player4
Step 11: Player4 >>> Player2
Step 12: Player2 >>> Player4
Step 13: Player4 >>> Player1
Step 14: Player1 >>> Player4
Step 15: Player4 >>> Player3
Step 16: Player3 >>> Player1
Step 17: Player1 >>> Player2
Step 18: Player2 >>> Player3
Step 19: Player3 >>> Player1
Step 20: Player1 >>> Player5
Step 21: Player5 >>> Player1
Step 22: Player1 >>> Player2
Step 23: Player2 >>> Player4
Step 24: Player4 >>> Player5
Step 25: Player5 >>> Player3
Step 26: Player3 >>> Player4
Step 27: Player4 >>> Player2
Step 28: Player2 >>> Player5
Step 29: Player5 >>> Player2
Step 30: Player2 >>> Player1
Step 31: Player1 >>> Player3
Step 32: Player3 >>> Player4
Step 33: Player4 >>> Player1
Step 34: Player1 >>> Player3
Step 35: Player3 >>> Player4
Step 36: Player4 >>> Player3
Step 37: Player3 >>> Player2
Step 38: Player2 >>> Player5
Step 39: Player5 >>> Player4
Step 40: Player4 >>> Player5
Step 41: Player5 >>> Player1
Step 42: Player1 >>> Player5
Step 43: Player5 >>> Player3
Step 44: Player3 >>> Player5
Step 45: Player5 >>> Player2
Step 46: Player2 >>> Player3
Step 47: Player3 >>> Player2
Step 48: Player2 >>> Player5
Step 49: Player5 >>> Player4
Step 50: Player4 >>> Player2
Step 51: Player2 >>> Player5
Step 52: Player5 >>> Player1
Step 53: Player1 >>> Player5
Step 54: Player5 >>> Player3
Step 55: Player3 >>> Player5
Step 56: Player5 >>> Player2
Step 57: Player2 >>> Player1
Step 58: Player1 >>> Player4
Step 59: Player4 >>> Player1
Step 60: Player1 >>> Player4
Step 61: Player4 >>> Player3
Step 62: Player3 >>> Player2
Step 63: Player2 >>> Player5
Step 64: Player5 >>> Player4
Step 65: Player4 >>> Player5
Step 66: Player5 >>> Player1
Step 67: Player1 >>> Player5
Step 68: Player5 >>> Player3
Step 69: Player3 >>> Player4
Step 70: Player4 >>> Player2
Step 71: Player2 >>> Player5
Step 72: Player5 >>> Player2
Step 73: Player2 >>> Player1
Step 74: Player1 >>> Player4
Step 75: Player4 >>> Player1
Step 76: Player1 >>> Player2
Step 77: Player2 >>> Player5
Step 78: Player5 >>> Player4
Step 79: Player4 >>> Player3
Step 80: Player3 >>> Player1
Step 81: Player1 >>> Player5
Step 82: Player5 >>> Player1
Step 83: Player1 >>> Player4
Step 84: Player4 >>> Player5
Step 85: Player5 >>> Player3
Step 86: Player3 >>> Player5
Step 87: Player5 >>> Player2
Step 88: Player2 >>> Player3
Player2: I'm out!
Step 89: Player3 >>> Player4
... player Player2 is out ...
Step 90: Player4 >>> Player1
Step 91: Player1 >>> Player3
Step 92: Player3 >>> Player1
Step 93: Player1 >>> Player4
Step 94: Player4 >>> Player3
Step 95: Player3 >>> Player5
Step 96: Player5 >>> Player1
Step 97: Player1 >>> Player5
Step 98: Player5 >>> Player3
Step 99: Player3 >>> Player5
Step 100: Player5 >>> Player4
Step 101: Player4 >>> Player5
Player4: I'm out!
... player Player4 is out ...
Step 102: Player5 >>> Player1
Step 103: Player1 >>> Player3
Step 104: Player3 >>> Player1
Step 105: Player1 >>> Player3
Step 106: Player3 >>> Player5
Step 107: Player5 >>> Player3
Step 108: Player3 >>> Player1
Step 109: Player1 >>> Player3
Step 110: Player3 >>> Player5
Step 111: Player5 >>> Player1
Step 112: Player1 >>> Player3
Step 113: Player3 >>> Player5
Step 114: Player5 >>> Player3
Step 115: Player3 >>> Player1
Step 116: Player1 >>> Player3
Step 117: Player3 >>> Player5
Step 118: Player5 >>> Player1
Step 119: Player1 >>> Player3
Step 120: Player3 >>> Player5
Step 121: Player5 >>> Player3
Player5: I'm out!
... player Player5 is out ...
Step 122: Player3 >>> Player5
Step 123: Player5 >>> Player1
Player5: I'm out!
Step 124: Player1 >>> Player3
... player Player5 is out ...
Step 125: Player3 >>> Player1
Step 126: Player1 >>> Player3
Player1: I'm out!
... player Player1 is out ...
Step 127: Player3 >>> Player3
Player3: I'm out!
Step 128: Player3 >>> Player3
... player Player3 is out ...
Player3: I'm out!
Stop!
Step 129: Player3 >>> Player3
Player3: I'm out!

Nga e gjithë kjo, mund të nxirren disa përfundime të rëndësishme:

  • në prani të mjeteve të nevojshme, zhvilluesit e aplikacioneve mund të krijojnë ndërveprime integruese midis aplikacioneve pa ndërprerë logjikën biznesore;
  • kompleksiteti (complexity) i detyrës integruese, që kërkon kompetenca inxhinierike, mund të fshihet brenda kornizës, nëse këtë e-sëj fillimisht në arkitekturën e kornizës. Vështirësia e detyrës (difficulty) nuk mund të fshihet, prandaj zgjidhja e një detyre të vështirë në kod do të duket përkatësisht;
  • Kur zhvilloni logjikën e integrimit, është thelbësore të merrni parasysh konsistencën e përhershme dhe mungesën e linearisimit të ndryshimeve të gjendjes së të gjitha palëve të integrimit. Kjo e detyron logjikën të komplikohet, për ta bërë atë të pandjeshme ndaj rendit të shfaqjes së ngjarjeve të jashtme. Në shembullin tonë, një lojtar është i detyruar të marrë pjesë në lojë vetëm pasi të shpallë daljen nga ajo: lojtarët e tjerë do të vazhdojnë t'i kalojnë atij topin derisa informacioni për daljen e tij të arrijë dhe të përpunojë nga të gjitha palët e interesuara. Kjo logjikë nuk rrjedh nga rregullat e lojës dhe është një zgjidhje kompromisi brenda arkitekturës së zgjedhur.

Më pas do të flasim për nuanca të ndryshme të zgjidhjes sonë, kompromise dhe çështje të tjera.

Të gjitha mesazhet – në një radhë

Të gjitha aplikacionet e integruara punojnë me një autobuz integrues, i cili paraqitet si një broker i jashtëm, një radhë BPMQueue – për mesazhe dhe një temë BPMTopic – për sinjalet (ngjarjet). Të kalosh të gjitha mesazhet përmes një radhë është vetë një kompromis. Në nivelin e logjikës biznesore, tani mund të disa lloje të reja mesazhesh pa bërë ndryshime në strukturën e sistemit. Kjo është një thjeshtësim i rëndësishëm, por ka disa rreziqe, të cilat në kontekstin e detyrave tona standarde na janë dukur të vogla.

Integrimi në stilin BPM

Megjithatë, ka një nuancë këtu: çdo aplikacion filtroi "mesazhet e tij" nga radhët që në hyrje, sipas emrit të domenit të tij. Gjithashtu, domeni mund të tregojë dhe në sinjale, nëse është e nevojshme të kufizosh "fushën e dukshmërisë" të sinjalit vetëm në një aplikacion të vetë. Kjo duhet të rrisë kapacitetin e autobuzit, por logjika biznesore tani duhet të operojë me emrat e domenëve: për adresimin e mesazheve – është e detyrueshme, për sinjalet – është e dëshirueshme.

Sigurimi i besueshmërisë së autobuzit integrues

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

  • broker i zgjedhur i mesazheve është një komponent kritik i arkitekturës dhe një pikë e vetme dështimi: ai duhet të jetë mjaft i qëndrueshëm ndaj dështimit. Duhet të përdoren vetëm implementime të provuara me kalimin e kohës, me mbështetje të mirë dhe një komunitet të madh;
  • duhet të sigurohet një disponueshmëri e lartë e brokerit të mesazheve, për të cilën ai duhet të jetë fizikisht i ndarë nga aplikacionet e integruara (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ë garantimin «të paktën një herë» për dërgimin. Ky është një kërkesë e detyrueshme për funksionimin e besueshëm të autobusit të integrimit. Nuk ka nevojë për garanci të nivelit «pikërisht një herë»: proceset e biznesit zakonisht nuk janë të ndjeshme ndaj përsëritjes së mesazheve ose ngjarjeve, dhe për detyra të veçanta, ku kjo është e rëndësishme, është më e thjeshtë të shtosh një kontroll shtesë në logjikën e biznesit sesa të përdorësh 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 të domainit. Një variant preferencial do të ishte përdorimi i një modeli Kodi i Transaksionit, por do të kërkojë një tabelë shtesë në bazën e të dhënave dhe një retransmetues. Në aplikacionet JEE mund të thjeshtohet ky moment duke përdorur menaxherin lokal JTA, por lidhja me brokerin e zgjedhur duhet të jetë në gjendje të funksionojë në modin XA;
  • menaxherët e mesazheve të ardhshme dhe ngjarjeve gjithashtu duhet të punojnë me transaksionin që ndryshon gjendjen e procesit të biznesit: nëse një transaksion i tillë anulohet, atëherë pranimi i mesazhit gjithashtu duhet të anulohet;
  • mesazhet që nuk arritën të dërgoheshin për shkak të gabimeve duhet të grumbullohen në një depo të veçantë DLQ (Dead Letter Queue). Ne për këtë krijuam një mikroshërbim të veçantë në platformë, i cili ruan këto mesazhe në depozitat e tij, i indekson ato sipas atributeve (për grupim dhe kërkimin e shpejtë), dhe ofron një API për shikimin, dërgimin përsëri në destinacion dhe fshirjen e mesazheve. Administratorët e sistemit mund të punojnë me këtë shërbim përmes ndërfaqes së tyre në internet;
  • në cilësimet e brokerit duhet të rregullohet numri i përpjekjeve për dorëzim dhe vonesat midis dorëzimeve, për të reduktuar probabilitetin e mesazheve që të përfundojnë në DLQ (llogaritja e parametrave optimalë është pothuajse e pamundur, por mund të veprohet në mënyrë empirike dhe t'i rregullojmë ato gjatë përdorimit);
  • ruajtja e DLQ duhet të monitorohet vazhdimisht, dhe sistemi i monitorimit duhet të njoftojë administratorët e sistemit, në mënyrë që të reagohet sa më shpejt përballë mesazheve të papërfunduara. Kjo do të ndihmojë në zvogëlimin e "zonës së prekur" nga dështimi ose gabimi i logjikës biznesore;
  • bordi integrues duhet të jetë i papërshkueshëm ndaj mungesës përkohësore të aplikacioneve: abonimet në temë duhet të jenë durable, dhe emri i domainit të aplikacionit duhet të jetë unik, që gjatë mungesës së aplikacionit, mesazhet e tij nga radhët të mos përpiqen të përpunohen nga dikush tjetër.

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

Një ekzemplar i një procesi biznesi mund të marrë disa mesazhe dhe ngjarje në të njëjtën kohë, përpunimi i të cilave do të nisë në paralel. Ndërkohë, për zhvilluesin e aplikacioneve, gjithçka duhet të jetë e thjeshtë dhe e sigurt për përdorim në rrjedhë.

Logjika e biznesit të procesit përpunon çdo ngjarje të jashtme që ndikon në këtë proces biznesi veç e veç. Këto ngjarje mund të jenë:

  • nisja e një ekzemplar të procesit të biznesit;
  • veprimi i përdoruesit që i përket aktiviteteve brenda procesit të biznesit;
  • pranimi i një mesazhi ose sinjali, në të cilin ekzemplari i procesit të biznesit është regjistruar;
  • aktivizimi i një timeri të vendosur nga ekzemplari i procesit të biznesit;
  • ndikimi menaxhues përmes API-së (p.sh., ndërprerja emergjente e procesit).

Çdo ngjarje e tillë mund të ndryshojë gjendjen e instancës së procesit të biznesit: disa aktivitete mund të përfundojnë dhe të tjera mund të fillojnë, ndërsa vlerat e vetive persistuese mund të ndryshojnë. Mbyllja e çdo aktiviteti mund të çojë në aktivizimin e një ose më shumë aktiviteteve që vijojnë. Ato, nga ana e tyre, mund të ndalen duke pritur ngjarje të tjera ose, nëse nuk kanë nevojë për të dhëna të mëtejshme, 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ë ndodhin e ngjarjes tjetër të jashtme.

Të dhënat persistuese të procesit të biznesit, të ruajtura në një DB relacional, janë një pikë shumë e përshtatshme për sinkronizimin e përpunimit nëse përdoret SELECT FOR UPDATE. Nëse një transaksion arrin të marrë gjendjen e procesit të biznesit nga baza për ta ndryshuar atë, asnjë transaksion tjetër nuk mund të marrë paralelisht të njëjtën gjendje për një ndryshim tjetër, dhe pasi të përfundojë transaksioni i parë, transaksioni i dytë garanton se do të marrë gjendjen e ndryshuar.

Duke përdorimi i bllokimeve pesimiste në anën e DBMS, ne përmbushim të gjitha kërkesat e nevojshme ACID, si dhe ruajmë mundësinë e shkallëzimit të aplikacionit me logjikën e biznesit duke rritur numrin e instancave të aktivizuara.

Megjithatë, bllokimet pesimiste na kanosen me dështime të bllokimeve, prandaj, SELECT FOR UPDATE gjithsesi duhet të kufizohet me një kohëzgjatje të arsyeshme në rast të dështimeve të bllokimeve në disa raste të dukshme në logjikën e biznesit.

Një tjetër problem është sinkronizimi i fillimit të procesit të biznesit. Ndërsa nuk ka një instancë të procesit të biznesit, nuk ka as statusin e tij në bazë, 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ë shkallë të caktuar, atëherë do të kërkohet një objekt sinkronizimi, i lidhur me klasën e procesit dhe shkallën përkatëse. Për të zgjidhur këtë problem, ne përdorim një mekanizëm tjetër bllokimi, i cili lejon marrjen e një bllokimi për një burim të rastësishëm, i cili caktohet me një çelës 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 janë prezente mesazhe për marrjen dhe lirimin e bllokimit të çelësit përkatës. Për proceset e tjera të biznesit, nuk ka të tillë mesazhe: uniqueConstraint nuk është i caktuar.

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

Ndonjëherë, praninë e gjendjes së qëndrueshme jo vetëm që e ndihmon, por gjithashtu e vështirëson zhvillimin.
Problemet fillojnë kur duhet 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ëja ndryshime të papajtueshme mund të sjellë shumë shqetësime, me të cilat ne shpesh kemi hasur kur kemi përdorur jBPM.

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

  1. të krijohet një lloj i ri procesi biznesi, që të mos bëhen ndryshime të papajtueshme në të vjetrin, dhe të përdoret ai në vend të të vjetrës gjatë nisjes së ekzemplarëve të rinj. Ekzemplarët e vjetër do të vazhdojnë të punojnë 'si më parë';
  2. të migrohet gjendja e qëndrueshme e proceseve të biznesit gjatë përditësimit të logjikës së biznesit.

Në fillim, rruga është më e thjeshtë, por ka kufizime dhe disavantazhe të saj, për shembull:

  • dyfishimi i logjikës biznesore në shumë modele të proceseve të biznesit, rritja e volit të logjikës biznesore;
  • shpesh kërkohet një kalim i menjëhershëm në logjikën e re të biznesit (në aspektin e detyrave të integrimit – pothuajse gjithmonë);
  • zhvilluesi nuk e di se në cilin moment mund të eliminohen modelet e vjetra.

Në praktikë, ne përdorim të dyja qasjet, por kemi marr disa vendime për të lehtësuar jetën tonë:

  • 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 për t'u përpunuar: në një rresht formati JSON. Kjo lejon kryerjen e migrimeve si brenda aplikacionit, ashtu edhe jashtë. Në rastin më të keq, mund të korrigjohet manualisht (veçanërisht e dobishme në zhvillim gjatë debug-imit);
  • 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 prej proceseve të përfshira me një të re, me një emër të ri (për shembull, “InitialPlayerV2”). Lidhja ndodh përmes emrave të mesazheve dhe sinjaleve;
  • modeli i procesit ka një numër versioni, i cili rritet 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 lexohet 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 përkrah logjikës biznesore dhe thirret 'përndryshe' për çdo instancë të procesit biznesor në momentin e rikuperimit të tij nga baza;
  • nëse është e nevojshme të migrohet gjendja e të gjitha instancave të procesit në mënyrë operative dhe të sinkronizuar, aplikohen zgjidhje më klasike për migrimin e DB, por atje duhet të punojmë me JSON.

A është i nevojshëm një tjetër kornizë për proceset biznesore?

Zgjidhjet e përshkruara në artikull na lejuan të thjeshtojmë ndjeshëm jetën tonë, të zgjeronim gamën e pyetjeve që zgjidhen në nivelin e zhvillimit aplikativ dhe të bënim idetë për ndarjen e logjikës biznesore në mikroshërbime më tërheqëse. Për këtë, u krye shumë punë, u krijua një kornizë shumë "të lehtë" për proceset biznesore, si dhe komponentë ndihmës për të zgjidhur problemet e përmendura në kontekstin e një gamë të gjerë detyrash aplikative. Ne kemi dëshirë të ndajmë këto rezultate, duke e çuar zhvillimin e komponentëve të përbashkët në publik nën licencë të lirë. Kjo do të kërkojë përpjekje dhe kohë të caktuara. Kuptimi i kërkesës për zgjidhje të tilla mund të ishte një stimul shtesë për ne. Në artikullin e propozuar i është kushtuar shumë pak vëmendje mundësive të vetë kornizës, por disa prej tyre janë të dukshme nga shembujt e paraqitur. Nëse vendosim të publikojmë kornizën tonë, do t'i kushtohet një artikull të veçantë. Deri atëherë do të ishim të mirënjohur nëse do të lini një feedback të vogël, duke u përgjigjur në pyetjen:

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

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

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

  • 12,5%është interesante të dish më shumë rreth zbatimit tuaj, mund të jetë i dobishëm

  • 6,2%po përdorim një nga kornizat ekzistuese, por po mendojmë të bëjmë një ndryshim

  • 18,8%po përdorim një nga kornizat ekzistuese, gjithçka na pëlqen

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

  • 25,0%po shkruajmë tonën

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

Burimi: habr.com

Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS 🔥 Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS | ProHoster