BPM-tyylinen integraatio

BPM-tyylinen integraatio

Hei, Habr!

Yrityksemme on erikoistunut ERP-luokan ohjelmistoratkaisujen kehittämiseen, joista leijonanosan ovat transaktiojärjestelmät, joissa on valtava määrä liiketoimintalogiikkaa ja dokumenttivirtaa a la EDMS. Tuotteidemme nykyiset versiot perustuvat JavaEE-teknologioihin, mutta kokeilemme aktiivisesti myös mikropalveluita. Yksi tällaisten ratkaisujen ongelmallisimmista alueista on erilaisten vierekkäisiin toimialueisiin kuuluvien alijärjestelmien integrointi. Integraatioongelmat ovat aina aiheuttaneet meille valtavaa päänsärkyä käyttämistämme arkkitehtonisista tyyleistä, teknologiapinoista ja kehyksistä riippumatta, mutta viime aikoina tällaisten ongelmien ratkaisemisessa on edistytty.

Huomioimassani artikkelissa kerron NPO Kristan kokemuksista ja arkkitehtonisesta tutkimuksesta osoitetulla alueella. Tarkastellaan myös esimerkkiä yksinkertaisesta ratkaisusta integraatioongelmaan sovelluskehittäjän näkökulmasta ja selvitetään, mitä tämän yksinkertaisuuden takana piilee.

Vastuuvapauslauseke

Artikkelissa kuvatut arkkitehtoniset ja tekniset ratkaisut olen ehdottanut henkilökohtaisten kokemusten perusteella tiettyjen tehtävien yhteydessä. Nämä ratkaisut eivät väitä olevansa universaaleja eivätkä välttämättä ole optimaalisia muissa käyttöolosuhteissa.

Mitä tekemistä BPM:llä on sen kanssa?

Vastataksemme tähän kysymykseen meidän on syvennettävä ratkaisujemme sovellettavien ongelmien erityispiirteisiin. Pääosa liiketoimintalogiikasta tyypillisessä tapahtumajärjestelmässämme on tietojen syöttäminen tietokantaan käyttöliittymien kautta, näiden tietojen manuaalinen ja automaattinen tarkastus, sen suorittaminen jonkin työnkulun kautta, julkaiseminen toiseen järjestelmään / analyyttiseen tietokantaan / arkistoon, raporttien luominen . Näin ollen järjestelmän keskeinen tehtävä asiakkaille on heidän sisäisten liiketoimintaprosessiensa automatisointi.

Mukavuuden vuoksi käytämme viestinnässä termiä "asiakirja" jonkinlaisena abstraktiona tietojoukosta, jota yhdistää yhteinen avain, johon tietty työnkulku voidaan "linkittää".
Mutta entä integraatiologiikka? Loppujen lopuksi integrointitehtävän tuottaa järjestelmän arkkitehtuuri, joka "leikataan" osiin EI asiakkaan pyynnöstä, vaan täysin erilaisten tekijöiden vaikutuksesta:

  • Conwayn lain alainen;
  • muille tuotteille aiemmin kehitettyjen osajärjestelmien uudelleenkäytön seurauksena;
  • arkkitehdin harkinnan mukaan, ei-toiminnallisten vaatimusten perusteella.

On suuri kiusaus erottaa integraatiologiikka päätyönkulun liiketoimintalogiikasta, jotta liiketoimintalogiikka ei saastuttaisi integraatioartefakteilla ja vapautettaisiin sovelluskehittäjän tarpeesta syventyä järjestelmän arkkitehtonisen maiseman erityispiirteisiin. Tällä lähestymistavalla on useita etuja, mutta käytäntö osoittaa sen tehottomuuden:

  • integrointiongelmien ratkaiseminen jää yleensä takaisin yksinkertaisimpiin vaihtoehtoihin synkronisten puheluiden muodossa, koska päätyönkulun toteutuksessa on rajoitettu laajennuspisteitä (synkronisen integraation haittoja käsitellään alla);
  • integrointiartefaktit tunkeutuvat edelleen ydinliiketoimintalogiikkaan, kun palautetta toisesta osajärjestelmästä tarvitaan;
  • sovelluskehittäjä jättää integroinnin huomioimatta ja voi helposti katkaista sen muuttamalla työnkulkua;
  • järjestelmä lakkaa olemasta yksi kokonaisuus käyttäjän näkökulmasta, osajärjestelmien väliset "saumat" tulevat havaittaviksi ja päälle tulee tarpeettomia käyttäjätoimintoja, jotka käynnistävät tiedon siirron osajärjestelmästä toiseen.

Toinen lähestymistapa on pitää integraatiovuorovaikutusta olennaisena osana ydinliiketoimintalogiikkaa ja työnkulkua. Jotta sovelluskehittäjien pätevyydet eivät nouse pilviin, uusien integraatiovuorovaikutusten luomisen tulisi olla helppoa ja vaivatonta, ja ratkaisun valintamahdollisuuksien tulee olla minimaaliset. Tämä on vaikeampi tehdä kuin miltä näyttää: työkalun on oltava riittävän tehokas tarjoamaan käyttäjälle tarvittavat vaihtoehdot sen käyttöön ilman, että hän "ampuisi itseään jalkaan". On monia kysymyksiä, joihin insinöörin on vastattava integrointitehtävien yhteydessä, mutta joita sovelluskehittäjän ei pitäisi ajatella päivittäisessä työssään: tapahtumarajat, johdonmukaisuus, atomisuus, turvallisuus, skaalaus, kuormitus ja resurssien jakautuminen, reititys, ryhmittely, jakelu- ja vaihtokontekstit jne. Sovelluskehittäjille on tarpeen tarjota melko yksinkertaisia ​​ratkaisumalleja, joissa vastaukset kaikkiin tällaisiin kysymyksiin ovat jo piilossa. Näiden mallien on oltava varsin turvallisia: liiketoimintalogiikka muuttuu hyvin usein, mikä lisää virheiden syntymisen riskiä, ​​virhekustannusten tulee pysyä melko alhaisella tasolla.

Mutta mitä tekemistä BPM:llä on sen kanssa? Työnkulun toteuttamiseen on monia vaihtoehtoja...
Toinen liiketoimintaprosessien toteutus onkin erittäin suosittu ratkaisuissamme - tilasiirtymäkaavion deklaratiivisen määrittelyn ja käsittelijöiden yhdistämisen kautta liikelogiikkaan siirtymiä varten. Tässä tapauksessa tila, joka määrittää "asiakirjan" nykyisen sijainnin liiketoimintaprosessissa, on itse "asiakirjan" attribuutti.

BPM-tyylinen integraatio
Tältä prosessi näyttää projektin alussa

Tämän toteutuksen suosio johtuu lineaaristen liiketoimintaprosessien luomisen suhteellisen yksinkertaisuudesta ja nopeudesta. Kuitenkin, kun ohjelmistojärjestelmät muuttuvat jatkuvasti monimutkaisemmiksi, liiketoimintaprosessin automatisoitu osa kasvaa ja monimutkaistuu. Tarvitaan hajottamista, prosessien osien uudelleenkäyttöä sekä haaroitusta niin, että jokainen haara suoritetaan rinnakkain. Tällaisissa olosuhteissa työkalusta tulee epämukava ja tilasiirtymäkaavio menettää tietosisältönsä (integraatiovuorovaikutukset eivät näy kaaviossa ollenkaan).

BPM-tyylinen integraatio
Tältä prosessi näyttää useiden vaatimusten selvennysten iteraatioiden jälkeen.

Tie ulos tästä tilanteesta oli moottorin integrointi jPPM joihinkin tuotteisiin, joissa on monimutkaisimmat liiketoimintaprosessit. Lyhyellä aikavälillä tällä ratkaisulla oli jonkin verran menestystä: tuli mahdolliseksi toteuttaa monimutkaisia ​​liiketoimintaprosesseja säilyttäen samalla melko informatiivinen ja relevantti kaavio merkinnässä BPMN2.

BPM-tyylinen integraatio
Pieni osa monimutkaista liiketoimintaprosessia

Ratkaisu ei pitkällä aikavälillä vastannut odotuksia: visuaalisten työkalujen avulla luotu liiketoimintaprosessien suuri työvoimaintensiteetti ei mahdollistanut hyväksyttävien tuottavuusindikaattoreiden saavuttamista, ja itse työkalusta tuli kehittäjien keskuudessa yksi inhotuimmista. Myös moottorin sisäisestä rakenteesta valitettiin, mikä johti monien "laastarien" ja "sauvojen" ilmestymiseen.

Pääasiallinen myönteinen puoli jBPM:n käytössä oli tietoisuus liiketoimintaprosessiinstanssin oman jatkuvan tilan eduista ja haitoista. Näimme myös mahdollisuuden käyttää prosessilähestymistapaa monimutkaisten integraatioprotokollien toteuttamiseen eri sovellusten välillä käyttämällä asynkronista vuorovaikutusta signaalien ja viestien kautta. Pysyvän tilan läsnäolo on tässä ratkaisevassa roolissa.

Edellä olevan perusteella voimme päätellä: BPM-tyylinen prosessilähestymistapa mahdollistaa monenlaisten tehtävien ratkaisemisen yhä monimutkaisempien liiketoimintaprosessien automatisoimiseksi, integrointitoimintojen sovittamiseksi harmonisesti näihin prosesseihin ja kyvyn esittää toteutettu prosessi visuaalisesti sopivalla merkinnällä.

Synkronisten puhelujen haitat integrointimallina

Synkronisella integraatiolla tarkoitetaan yksinkertaisinta estopuhelua. Yksi alijärjestelmä toimii palvelinpuolena ja paljastaa API:n vaaditulla menetelmällä. Toinen alijärjestelmä toimii asiakaspuolena ja soittaa oikeaan aikaan ja odottaa tulosta. Järjestelmäarkkitehtuurista riippuen asiakas- ja palvelinpuoli voivat sijaita joko samassa sovelluksessa ja prosessissa tai eri. Toisessa tapauksessa sinun on käytettävä jotakin RPC-toteutusta ja järjestettävä parametrien ja kutsun tulos.

BPM-tyylinen integraatio

Tällä integrointikuviolla on melko suuri joukko haittoja, mutta sitä käytetään käytännössä hyvin laajasti sen yksinkertaisuuden vuoksi. Toteutuksen nopeus kiehtoo ja pakottaa sinut käyttämään sitä yhä uudelleen tiukkojen määräaikojen edessä ja kirjaamalla ratkaisun tekniseksi velaksi. Mutta tapahtuu myös niin, että kokemattomat kehittäjät käyttävät sitä tiedostamatta, yksinkertaisesti ymmärtämättä kielteisiä seurauksia.

Ilmeisimmän osajärjestelmän liitettävyyden lisääntymisen lisäksi on myös vähemmän ilmeisiä ongelmia "kasvavissa" ja "venyttävissä" tapahtumissa. Itse asiassa, jos liiketoimintalogiikka tekee joitain muutoksia, tapahtumia ei voida välttää, ja tapahtumat puolestaan ​​estävät tietyt sovellusresurssit, joihin nämä muutokset vaikuttavat. Eli ennen kuin yksi alijärjestelmä odottaa vastausta toiselta, se ei voi suorittaa tapahtumaa loppuun ja poistaa lukot. Tämä lisää merkittävästi erilaisten vaikutusten riskiä:

  • Järjestelmän reagointikyky menetetään, käyttäjät odottavat pitkään vastauksia pyyntöihin;
  • palvelin lakkaa yleensä vastaamasta käyttäjien pyyntöihin ylikuormitetun säikeen vuoksi: suurin osa säikeistä on lukittu tapahtuman varaamaan resurssiin;
  • Umpikuja alkaa ilmaantua: niiden esiintymisen todennäköisyys riippuu voimakkaasti transaktioiden kestosta, liikelogiikan määrästä ja tapahtumaan liittyvistä lukoista;
  • tapahtuman aikakatkaisuvirheet ilmestyvät;
  • palvelin "epäonnistuu" OutOfMemoryn kanssa, jos tehtävä edellyttää suurten tietomäärien käsittelyä ja muuttamista, ja synkronisten integraatioiden läsnäolo vaikeuttaa käsittelyn jakamista "kevyempiin" tapahtumiin.

Arkkitehtonisesti katsottuna estopuheluiden käyttö integroinnin aikana johtaa yksittäisten osajärjestelmien laadun hallinnan menettämiseen: on mahdotonta varmistaa yhden osajärjestelmän tavoitelaatuindikaattoreita erillään toisen osajärjestelmän laatuindikaattoreista. Jos alijärjestelmiä kehittävät eri tiimit, tämä on suuri ongelma.

Asiat muuttuvat vielä mielenkiintoisemmiksi, jos integroitavat osajärjestelmät ovat eri sovelluksissa ja joudut tekemään synkronisia muutoksia molemmille puolille. Kuinka varmistaa näiden muutosten transaktiokyky?

Jos muutoksia tehdään erillisiin tapahtumiin, sinun on tarjottava luotettava poikkeuskäsittely ja kompensointi, mikä eliminoi täysin synkronisten integraatioiden pääedun - yksinkertaisuuden.

Hajautetut tapahtumat tulevat myös mieleen, mutta emme käytä niitä ratkaisuissamme: luotettavuutta on vaikea varmistaa.

"Saaga" ratkaisuna kaupankäyntiongelmaan

Mikropalveluiden suosion kasvaessa kysyntä kasvaa Saagan kuvio.

Tämä malli ratkaisee täydellisesti yllä mainitut pitkien transaktioiden ongelmat ja laajentaa myös järjestelmän tilan hallinnan mahdollisuuksia liiketoimintalogiikan puolelta: epäonnistuneen tapahtuman jälkeinen korvaus ei välttämättä vie järjestelmää takaisin alkuperäiseen tilaan, mutta tarjoaa vaihtoehtoinen tietojenkäsittelyreitti. Näin voit myös välttää onnistuneesti suoritettujen tietojenkäsittelyvaiheiden toistamisen, kun yrität saattaa prosessin "hyvään" loppuun.

Mielenkiintoista on, että monoliittisissa järjestelmissä tämä malli on merkityksellinen myös löyhästi kytkettyjen osajärjestelmien integroinnissa ja havaitaan pitkäaikaisten tapahtumien ja vastaavien resurssilukkojen aiheuttamia negatiivisia vaikutuksia.

Suhteessa BPM-tyylisiin liiketoimintaprosesseihimme ”Sagan” toteuttaminen osoittautuu erittäin helpoksi: ”Sagan” yksittäiset vaiheet voidaan määritellä liiketoimintaprosessin toiminnoiksi, ja myös liiketoimintaprosessin jatkuva tila määrittää "Sagan" sisäisen tilan. Toisin sanoen emme vaadi ylimääräisiä koordinointimekanismeja. Tarvitset vain viestinvälittäjän, joka tukee "vähintään kerran" takuita kuljetuksena.

Mutta tällä ratkaisulla on myös oma "hintansa":

  • liiketoimintalogiikka monimutkaistuu: korvaukset on selvitettävä;
  • on välttämätöntä luopua täydellisestä johdonmukaisuudesta, mikä voi olla erityisen herkkä monoliittisille järjestelmille;
  • Arkkitehtuuri muuttuu hieman monimutkaisemmaksi ja ylimääräinen tarve viestivälittäjälle ilmaantuu;
  • lisäseuranta- ja hallintatyökaluja tarvitaan (vaikka yleisesti ottaen tämä on hyvä: järjestelmäpalvelun laatu paranee).

Monoliittisissa järjestelmissä "Sag":n käytön perusteet eivät ole niin ilmeisiä. Mikropalveluissa ja muissa SOA-palveluissa, joissa todennäköisimmin on jo välittäjä ja täydellinen johdonmukaisuus uhrataan projektin alussa, tämän mallin hyödyt voivat olla huomattavasti haittoja suuremmat, varsinkin jos liiketoimintalogiikassa on kätevä API taso.

Liiketoiminnan logiikan kapselointi mikropalveluihin

Kun aloimme kokeilemaan mikropalveluita, heräsi järkevä kysymys: mihin sijoitetaan verkkotunnuksen liiketoimintalogiikka suhteessa verkkotunnuksen datan pysyvyyttä varmistavaan palveluun?

Erilaisten BPMS-järjestelmien arkkitehtuuria tarkasteltaessa saattaa tuntua järkevältä erottaa liiketoimintalogiikka pysyvyydestä: luoda alustasta ja toimialueesta riippumattomia mikropalveluita, jotka muodostavat ympäristön ja kontin toimialueen liiketoimintalogiikan toteuttamiselle, ja suunnitella toimialueen tietojen pysyvyys erillinen kerros erittäin yksinkertaisia ​​ja kevyitä mikropalveluita. Liiketoimintaprosessit suorittavat tässä tapauksessa pysyvyyskerroksen palvelujen organisoinnin.

BPM-tyylinen integraatio

Tällä lähestymistavalla on erittäin suuri etu: voit lisätä alustan toimivuutta niin paljon kuin haluat, ja vain vastaava alustan mikropalvelukerros lihoaa tästä. Liiketoimintaprosessit mistä tahansa toimialueesta voivat heti käyttää alustan uusia toimintoja heti, kun se päivitetään.

Yksityiskohtaisempi tutkimus paljasti tämän lähestymistavan merkittäviä haittoja:

  • alustapalvelu, joka toteuttaa useiden toimialueiden liiketoimintalogiikkaa kerralla, sisältää suuria riskejä yhtenä epäonnistumispisteenä. Säännölliset muutokset liiketoimintalogiikassa lisäävät virheiden riskiä, ​​jotka johtavat järjestelmän laajuisiin epäonnistumisiin;
  • suorituskykyongelmat: liiketoimintalogiikka toimii tietojensa kanssa kapean ja hitaan käyttöliittymän kautta:
    • tiedot kerätään jälleen ja pumpataan verkkopinon läpi;
    • verkkoaluepalvelu tarjoaa usein enemmän dataa kuin mitä liiketoimintalogiikka tarvitsee käsitelläkseen, koska palvelun ulkoisen API:n tasolla ei ole riittävästi kykyä parametroida pyyntöjä;
    • useat itsenäiset liiketoimintalogiikan osat voivat toistuvasti pyytää samoja tietoja uudelleen käsiteltäväksi (tätä ongelmaa voidaan lieventää lisäämällä istuntokomponentteja, jotka tallentavat dataa välimuistiin, mutta tämä mutkistaa arkkitehtuuria entisestään ja aiheuttaa datan merkityksellisyyden ja välimuistin mitätöimisen ongelmia);
  • kaupankäyntiongelmat:
    • Liiketoimintaprosessit, joiden tila on pysyvä ja jonka alustapalvelu tallentaa, ovat ristiriidassa toimialueen tietojen kanssa, eikä tämän ongelman ratkaisemiseksi ole helppoja tapoja;
    • verkkotunnuksen tietojen eston sijoittaminen tapahtuman ulkopuolelle: jos verkkotunnuksen liiketoimintalogiikassa on tehtävä muutoksia sen jälkeen, kun on ensin tarkistettu nykyisten tietojen oikeellisuus, on suljettava pois mahdollisuus kilpaillulle muutokselle käsitellyissä tiedoissa. Ulkoinen tietojen esto voi auttaa ratkaisemaan ongelman, mutta tällainen ratkaisu sisältää lisäriskejä ja heikentää järjestelmän yleistä luotettavuutta;
  • lisäongelmia päivityksessä: joissain tapauksissa pysyvyyspalvelu ja liiketoimintalogiikka on päivitettävä synkronisesti tai tiukasti järjestyksessä.

Lopulta meidän piti palata perusasioihin: kapseloida verkkotunnuksen tiedot ja toimialueen liiketoimintalogiikka yhdeksi mikropalveluksi. Tämä lähestymistapa yksinkertaistaa mikropalvelun käsitystä järjestelmän kiinteänä osana eikä aiheuta yllä olevia ongelmia. Tätä ei myöskään anneta ilmaiseksi:

  • API-standardointia tarvitaan vuorovaikutuksessa liiketoimintalogiikan kanssa (erityisesti käyttäjien toimien tarjoamiseksi osana liiketoimintaprosesseja) ja API-alustapalveluita varten; vaatii tarkempaa huomiota API-muutoksiin, eteenpäin ja taaksepäin yhteensopivuuteen;
  • on tarpeen lisätä ylimääräisiä ajonaikaisia ​​kirjastoja liikelogiikan toiminnan varmistamiseksi osana jokaista tällaista mikropalvelua, mikä aiheuttaa tällaisille kirjastoille uusia vaatimuksia: keveys ja transitiivisten riippuvuuksien vähimmäismäärä;
  • liiketoimintalogiikan kehittäjien on seurattava kirjastoversioita: jos mikropalvelua ei ole viimeistelty pitkään aikaan, se sisältää todennäköisesti vanhentuneen version kirjastoista. Tämä voi olla odottamaton este uuden ominaisuuden lisäämiselle ja saattaa edellyttää tällaisen palvelun vanhan liiketoimintalogiikan siirtämistä kirjastojen uusiin versioihin, jos versioiden välillä on yhteensopimattomia muutoksia.

BPM-tyylinen integraatio

Tällaisessa arkkitehtuurissa on myös alustapalvelukerros, mutta tämä kerros ei enää muodosta konttia toimialueen liiketoimintalogiikan toteuttamiselle, vaan vain sen ympäristön, joka tarjoaa aputoimintoja "alustalle". Tällaista kerrosta tarvitaan paitsi verkkoalueen mikropalvelujen kevyen luonteen ylläpitämiseksi, myös hallinnan keskittämiseksi.

Esimerkiksi käyttäjien toiminta liiketoimintaprosesseissa synnyttää tehtäviä. Tehtävien kanssa työskennellessään käyttäjän on kuitenkin nähtävä tehtävät kaikilta toimialueilta yleisessä luettelossa, mikä tarkoittaa, että siellä on oltava vastaava alustan tehtävärekisteröintipalvelu, joka on puhdistettu toimialueen liiketoimintalogiikasta. Liiketoiminnan logiikan kapseloinnin säilyttäminen tällaisessa kontekstissa on melko ongelmallista, ja tämä on toinen tämän arkkitehtuurin kompromissi.

Liiketoimintaprosessien integrointi sovelluskehittäjän silmin

Kuten edellä mainittiin, sovelluskehittäjä tulee irrottaa useiden sovellusten vuorovaikutuksen toteuttamisen teknisistä ja teknisistä ominaisuuksista, jotta voidaan luottaa hyvään kehittämisen tuottavuuteen.

Yritetään ratkaista melko vaikea integrointiongelma, joka on erityisesti keksitty artikkelia varten. Tämä on "peli" -tehtävä, joka sisältää kolme sovellusta, joissa jokainen määrittelee tietyn verkkotunnuksen: "app1", "app2", "app3".

Jokaisen sovelluksen sisällä käynnistetään liiketoimintaprosessit, jotka alkavat "pelata palloa" integraatioväylän kautta. Viestit, joiden nimi on "Ball", toimivat pallona.

Pelin säännöt:

  • ensimmäinen pelaaja on aloitteentekijä. Hän kutsuu muita pelaajia peliin, aloittaa pelin ja voi lopettaa sen milloin tahansa;
  • muut pelaajat ilmoittavat osallistumisestaan ​​peliin, "tuntevat" toisensa ja ensimmäisen pelaajan;
  • saatuaan pallon pelaaja valitsee toisen osallistuvan pelaajan ja syöttää pallon hänelle. Lähetysten kokonaismäärä lasketaan;
  • Jokaisella pelaajalla on "energiaa", joka vähenee jokaisen pelaajan pallonheiton myötä. Kun energia loppuu, pelaaja jättää pelin ja ilmoittaa eroavansa;
  • jos pelaaja jätetään yksin, hän ilmoittaa välittömästi lähtevänsä;
  • Kun kaikki pelaajat putoavat, ensimmäinen pelaaja julistaa pelin päättyneeksi. Jos hän lähtee pelistä aikaisin, hän jää seuraamaan peliä saadakseen sen loppuun.

Tämän ongelman ratkaisemiseksi käytän liiketoimintaprosessien DSL:ämme, jonka avulla voimme kuvata Kotlinin logiikkaa tiiviisti, minimaalisesti.

Ensimmäisen pelaajan (eli pelin aloittajan) liiketoimintaprosessi toimii app1-sovelluksessa:

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

// Это класс экземпляра процесса: инкапсулирует его внутреннее состояние
class InitialPlayer : ProcessImpl<InitialPlayer>(initialPlayerModel) {
    var playerName: String by persistent("Player1")
    var energy: Int by persistent(30)
    var players: PlayersList by persistent(PlayersList())
    var shotCounter: Int = 0
}

// Это декларация модели процесса: создается один раз, используется всеми
// экземплярами процесса соответствующего класса
val initialPlayerModel = processModel<InitialPlayer>(name = "InitialPlayer",
                                                     version = 1) {

    // По правилам, первый игрок является инициатором игры и должен быть единственным
    uniqueConstraint = UniqueConstraints.singleton

    // Объявляем активности, из которых состоит бизнес-процесс
    val sendNewGameSignal = signal<String>("NewGame")
    val sendStopGameSignal = signal<String>("StopGame")
    val startTask = humanTask("Start") {
        taskOperation {
            processCondition { players.size > 0 }
            confirmation { "Подключилось ${players.size} игроков. Начинаем?" }
        }
    }
    val stopTask = humanTask("Stop") {
        taskOperation {}
    }
    val waitPlayerJoin = signalWait<String>("PlayerJoin") { signal ->
        players.add(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... join player ${signal.data} ...")
    }
    val waitPlayerOut = signalWait<String>("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... player ${signal.data} is out ...")
    }
    val sendPlayerOut = signal<String>("PlayerOut") {
        signalData = { playerName }
    }
    val sendHandshake = messageSend<String>("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val throwStartBall = messageSend<Int>("Ball") {
        messageData = { 1 }
        activation = { selectNextPlayer() }
    }
    val throwBall = messageSend<Int>("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData<Int>("Ball") {
        shotCounter = it
    }

    // Теперь конструируем граф процесса из объявленных активностей
    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)
    }

    // Навешаем на активности дополнительные обработчики для логирования
    sendNewGameSignal.onExit { println("Let's play!") }
    sendStopGameSignal.onExit { println("Stop!") }
    sendPlayerOut.onExit { println("$playerName: I'm out!") }
}

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

Liikelogiikan suorittamisen lisäksi yllä oleva koodi voi tuottaa liiketoimintaprosessista oliomallin, joka voidaan visualisoida kaavion muodossa. Emme ole vielä toteuttaneet visualisaattoria, joten jouduimme käyttämään vähän aikaa sen piirtämiseen (tässä yksinkertaistin hieman BPMN-merkintää koskien porttien käyttöä parantaakseni kaavion johdonmukaisuutta annetun koodin kanssa):

BPM-tyylinen integraatio

app2 sisältää toisen pelaajan liiketoimintaprosessin:

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

class RandomPlayer : ProcessImpl<RandomPlayer>(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<RandomPlayer>(name = "RandomPlayer", 
                                                   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<RandomPlayer, Int>.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}")
}

Kaavio:

BPM-tyylinen integraatio

App3-sovelluksessa tehdään pelaaja, jolla on hieman erilainen käyttäytyminen: sen sijaan, että valitsee satunnaisesti seuraavan pelaajan, hän toimii round-robin-algoritmin mukaan:

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

Muuten pelaajan käyttäytyminen ei eroa edellisestä, joten kaavio ei muutu.

Nyt tarvitsemme testin suorittaaksemme kaiken tämän. Annan vain itse testin koodin, jotta en sotke artikkelia kattilalevyllä (itse asiassa käytin aiemmin luotua testiympäristöä testatakseni muiden liiketoimintaprosessien integrointia):

testipeli()

@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");
    // Теперь нужно немного подождать, пока игроки "познакомятся" друг с другом.
    // Ждать через sleep - плохое решение, зато самое простое. 
    // Не делайте так в серьезных тестах!
    Thread.sleep(1000);
    // Запускаем игру, закрывая пользовательскую активность
    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<String, Object> playerParams(String name, int energy) {
    Map<String, Object> params = new HashMap<>();
    params.put("playerName", name);
    params.put("energy", energy);
    return params;
}

Suoritetaan testi ja katsotaan lokia:

konsolin lähtö

Взята блокировка ключа 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!

Kaikesta tästä voimme tehdä useita tärkeitä johtopäätöksiä:

  • tarvittavilla työkaluilla sovelluskehittäjät voivat luoda integraatiovuorovaikutuksia sovellusten välillä keskeyttämättä liiketoimintalogiikkaa;
  • insinööriosaamista vaativan integrointitehtävän monimutkaisuus voidaan piilottaa viitekehyksen sisään, jos se on alun perin sisällytetty viitekehyksen arkkitehtuuriin. Ongelman vaikeutta ei voi piilottaa, joten vaikean ongelman ratkaisu koodissa näyttää siltä;
  • Integraatiologiikkaa kehitettäessä on ehdottomasti otettava huomioon mahdollinen johdonmukaisuus ja kaikkien integraatioon osallistujien tilan muutosten linearisoitavuuden puute. Tämä pakottaa meidät monimutkaisemaan logiikkaa, jotta se ei vaikuta ulkoisten tapahtumien järjestykseen. Esimerkissämme pelaaja pakotetaan osallistumaan peliin sen jälkeen, kun hän on ilmoittanut lopettavansa pelistä: muut pelaajat jatkavat pallon syöttämistä hänelle, kunnes tieto hänen poistumisestaan ​​saapuu ja kaikki osallistujat käsittelevät sen. Tämä logiikka ei seuraa pelisäännöistä ja on kompromissiratkaisu valitun arkkitehtuurin puitteissa.

Seuraavaksi puhumme ratkaisumme monimutkaisuudesta, kompromisseista ja muista kohdista.

Kaikki viestit ovat samassa jonossa

Kaikki integroidut sovellukset toimivat yhdellä integrointiväylällä, joka esitetään ulkoisen välittäjän, yhden BPMQueue-viestien ja yhden BPMTopic-aiheen muodossa signaaleille (tapahtumille). Kaikkien viestien asettaminen yhteen jonoon on itsessään kompromissi. Liikelogiikkatasolla voit nyt ottaa käyttöön niin monta uutta viestityyppiä kuin haluat ilman muutoksia järjestelmän rakenteeseen. Tämä on merkittävä yksinkertaistus, mutta siihen liittyy tiettyjä riskejä, jotka tyypillisten tehtäviemme yhteydessä eivät meistä tuntuneet niin merkittäviltä.

BPM-tyylinen integraatio

Tässä on kuitenkin yksi hienous: jokainen sovellus suodattaa "sanoman" viestinsä sisäänkäynnin jonosta verkkotunnuksensa nimen mukaan. Toimialue voidaan määrittää myös signaaleissa, jos signaalin "näkyvyysalue" on rajoitettava yhteen sovellukseen. Tämän pitäisi lisätä väylän kapasiteettia, mutta liiketoimintalogiikan on nyt toimittava verkkotunnuksilla: viestien osoittelemiseksi - pakollinen, signaaleille - toivottavaa.

Integraatioväylän luotettavuuden varmistaminen

Luotettavuus koostuu useista kohdista:

  • Valittu sanomanvälittäjä on arkkitehtuurin kriittinen komponentti ja yksi vikapiste: sen on oltava riittävän vikasietoinen. Sinun tulisi käyttää vain aika-testattuja toteutuksia, joilla on hyvä tuki ja laaja yhteisö;
  • on tarpeen varmistaa viestinvälittäjän korkea käytettävyys, jota varten se on fyysisesti erotettava integroiduista sovelluksista (sovelletun liiketoimintalogiikan sovellusten korkea saatavuus on paljon vaikeampaa ja kalliimpaa varmistaa);
  • välittäjä on velvollinen antamaan "vähintään kerran" toimitustakuun. Tämä on pakollinen vaatimus integrointiväylän luotettavalle toiminnalle. "Tasa kerran"-tason takuita ei tarvita: liiketoimintaprosessit eivät pääsääntöisesti ole herkkiä viestien tai tapahtumien toistuville saapumisille ja erikoistehtävissä, joissa tämä on tärkeää, on helpompi lisätä lisätarkistuksia yritykseen. logiikka kuin käyttää jatkuvasti melko "kalleita" "takuita;
  • viestien ja signaalien lähettämisen tulee olla mukana kokonaistapahtumassa, jossa liiketoimintaprosessien ja toimialueen tietojen tila muuttuu. Suositeltu vaihtoehto olisi käyttää kuviota Tapahtuman Lähtevät-kansio, mutta se vaatii lisätaulukon tietokannassa ja toistimen. JEE-sovelluksissa tätä voidaan yksinkertaistaa käyttämällä paikallista JTA-päällikköä, mutta yhteyden valittuun välittäjään tulee toimia XA;
  • saapuvien viestien ja tapahtumien käsittelijöiden on työskenneltävä myös tapahtuman kanssa, joka muuttaa liiketoimintaprosessin tilaa: jos tällainen tapahtuma peruutetaan, viestin vastaanotto on peruutettava;
  • viestit, joita ei voitu toimittaa virheiden vuoksi, on tallennettava erilliseen varastoon D.L.Q. (Kuollut kirjejono). Tätä tarkoitusta varten loimme erillisen alustan mikropalvelun, joka tallentaa tällaiset viestit omaan muistiinsa, indeksoi ne attribuuttien mukaan (nopeaa ryhmittelyä ja hakua varten) ja paljastaa API:n viestien katselua, uudelleenlähetystä kohdeosoitteeseen ja poistamista varten. Järjestelmänvalvojat voivat työskennellä tämän palvelun kanssa verkkokäyttöliittymänsä kautta.
  • välittäjäasetuksissa sinun on säädettävä toimitusten uudelleenyritysten lukumäärää ja toimitusten välisiä viiveitä vähentääksesi todennäköisyyttä, että viestit pääsevät DLQ: hen (optimaalisia parametreja on lähes mahdotonta laskea, mutta voit toimia empiirisesti ja säätää niitä käytön aikana );
  • DLQ-säilöä on valvottava jatkuvasti ja valvontajärjestelmän on varoitettava järjestelmänvalvojat, jotta he voivat vastata toimittamattomien viestien sattuessa mahdollisimman nopeasti. Tämä vähentää vian tai liiketoimintalogiikan virheen "vaikutusaluetta";
  • integraatioväylän tulee olla epäherkkä sovellusten väliaikaiselle poissaolleelle: aiheen tilausten on oltava kestäviä ja sovelluksen verkkotunnuksen on oltava yksilöllinen, jotta sovelluksen poissa ollessa joku muu ei yritä käsitellä sen viestejä jonottaa.

Liiketoiminnan logiikan lankojen turvallisuuden varmistaminen

Sama liiketoimintaprosessin esiintymä voi vastaanottaa useita viestejä ja tapahtumia kerralla, joiden käsittely alkaa rinnakkain. Samaan aikaan sovelluskehittäjälle kaiken pitäisi olla yksinkertaista ja säikeen turvallista.

Prosessin liiketoimintalogiikka käsittelee jokaisen ulkoisen tapahtuman, joka vaikuttaa kyseiseen liiketoimintaprosessiin erikseen. Tällaisia ​​tapahtumia voivat olla:

  • liiketoimintaprosessin käynnistäminen;
  • käyttäjän toiminta, joka liittyy toimintaan liiketoimintaprosessissa;
  • viestin tai signaalin vastaanottaminen, jolle liiketoimintaprosessin ilmentymä on tilattu;
  • liiketoimintaprosessiinstanssin asettaman ajastimen laukaisu;
  • ohjata toimintaa API:n kautta (esimerkiksi prosessin keskeytys).

Jokainen tällainen tapahtuma voi muuttaa liiketoimintaprosessin ilmentymän tilaa: jotkut toiminnot voivat päättyä ja toiset alkavat, ja pysyvien ominaisuuksien arvot voivat muuttua. Minkä tahansa toiminnon sulkeminen voi johtaa yhden tai useamman seuraavista toiminnoista aktivoitumiseen. Ne puolestaan ​​voivat lopettaa muiden tapahtumien odottamisen tai, jos he eivät tarvitse lisätietoja, voivat suorittaa saman tapahtuman. Ennen tapahtuman sulkemista liiketoimintaprosessin uusi tila tallennetaan tietokantaan, jossa se odottaa seuraavan ulkoisen tapahtuman tapahtumista.

Relaatiotietokantaan tallennetut pysyvät liiketoimintaprosessitiedot ovat erittäin kätevä tapa synkronoida käsittelyä, jos käytät SELECT FOR UPDATE -toimintoa. Jos yksi transaktio onnistui saamaan liiketoimintaprosessin tilan sen muuttamiskannasta, niin mikään muu rinnakkainen tapahtuma ei voi saada samaa tilaa toista muutosta varten, ja ensimmäisen tapahtuman suorittamisen jälkeen toinen on saa taatusti jo muuttuneen tilan.

Käyttämällä pessimistisiä lukkoja DBMS-puolella täytämme kaikki tarvittavat vaatimukset ACID, ja säilyttää myös mahdollisuuden skaalata sovellus liiketoimintalogiikalla lisäämällä käynnissä olevien esiintymien määrää.

Pessimistiset lukot uhkaavat kuitenkin meitä umpikujalla, mikä tarkoittaa, että SELECT FOR UPDATE tulisi silti rajoittaa kohtuulliseen aikakatkaisuun siltä varalta, että joissakin liikelogiikan räikeissä tapauksissa umpikuja tulee.

Toinen ongelma on liiketoimintaprosessin alun synkronointi. Vaikka liiketoimintaprosessia ei ole esiintynyt, tietokannassa ei ole tilaa, joten kuvattu menetelmä ei toimi. Jos haluat varmistaa liiketoimintaprosessin ilmentymän ainutlaatuisuuden tietyssä laajuudessa, tarvitset jonkinlaisen synkronointiobjektin, joka liittyy prosessiluokkaan ja vastaavaan laajuuteen. Tämän ongelman ratkaisemiseksi käytämme erilaista lukitusmekanismia, jonka avulla voimme lukita mielivaltaisen resurssin, joka on määritetty avaimella URI-muodossa ulkoisen palvelun kautta.

Esimerkeissämme InitialPlayer-liiketoimintaprosessi sisältää ilmoituksen

uniqueConstraint = UniqueConstraints.singleton

Siksi loki sisältää viestejä vastaavan avaimen lukon ottamisesta ja vapauttamisesta. Tällaisia ​​viestejä ei ole muille liiketoimintaprosesseille: ainutlaatuista rajoitusta ei ole asetettu.

Liiketoimintaprosessien ongelmat jatkuvassa tilassa

Joskus jatkuva tila ei vain auta, vaan myös todella estää kehitystä.
Ongelmat alkavat, kun liiketoimintalogiikkaan ja/tai liiketoimintaprosessimalliin on tehtävä muutoksia. Jokainen tällainen muutos ei ole yhteensopiva liiketoimintaprosessien vanhan tilan kanssa. Jos tietokannassa on useita eläviä ilmentymiä, yhteensopimattomien muutosten tekeminen voi aiheuttaa paljon ongelmia, joita kohtasimme usein jBPM:ää käytettäessä.

Muutosten syvyydestä riippuen voit toimia kahdella tavalla:

  1. Luo uusi liiketoimintaprosessityyppi, jotta et tee yhteensopimattomia muutoksia vanhaan, ja käytä sitä vanhan sijaan käynnistäessäsi uusia ilmentymiä. Vanhat kopiot toimivat edelleen "kuten ennen";
  2. siirrä liiketoimintaprosessien jatkuva tila liiketoimintalogiikkaa päivittäessäsi.

Ensimmäinen tapa on yksinkertaisempi, mutta sillä on rajoituksensa ja haitansa, esimerkiksi:

  • bisneslogiikan päällekkäisyyksiä monissa liiketoimintaprosessimalleissa, mikä lisää liiketoimintalogiikan määrää;
  • Usein vaaditaan välitöntä siirtymistä uuteen liiketoimintalogiikkaan (integraatiotehtävien suhteen - melkein aina);
  • kehittäjä ei tiedä, missä vaiheessa vanhentuneet mallit voidaan poistaa.

Käytämme käytännössä molempia lähestymistapoja, mutta olemme tehneet useita päätöksiä helpottaaksemme elämäämme:

  • Tietokantaan liiketoimintaprosessin pysyvä tila on tallennettu helposti luettavassa ja helposti prosessoitavassa muodossa: JSON-muotoiseen merkkijonoon. Tämä mahdollistaa siirrot sekä sovelluksen sisällä että ulkoisesti. Viimeisenä keinona voit korjata sen manuaalisesti (erityisen hyödyllinen kehitystyössä virheenkorjauksen aikana);
  • integraation liiketoimintalogiikka ei käytä liiketoimintaprosessien nimiä, joten yhden osallistuvan prosessin toteutus voidaan milloin tahansa korvata uudella uudella nimellä (esim. "InitialPlayerV2"). Sitoutuminen tapahtuu sanomien ja signaalien nimien kautta;
  • prosessimallilla on versionumero, jota lisäämme, jos teemme tähän malliin yhteensopimattomia muutoksia, ja tämä numero tallennetaan yhdessä prosessiinstanssin tilan kanssa;
  • prosessin pysyvä tila luetaan tietokannasta ensin kätevään objektimalliin, jonka kanssa siirtoproseduuri voi toimia, jos mallin versionumero on muuttunut;
  • siirtomenettely on sijoitettu liiketoimintalogiikan viereen, ja sitä kutsutaan "laiskaksi" jokaiselle liiketoimintaprosessin esiintymälle, kun se palautetaan tietokannasta;
  • Jos sinun on siirrettävä kaikkien prosessiinstanssien tila nopeasti ja synkronisesti, käytetään klassisempia tietokannan siirtoratkaisuja, mutta sinun on työskenneltävä JSONin kanssa.

Tarvitsetko toisen viitekehyksen liiketoimintaprosesseille?

Artikkelissa kuvattujen ratkaisujen avulla pystyimme yksinkertaistamaan elämäämme merkittävästi, laajentamaan sovelluskehitystasolla ratkaistujen ongelmien kirjoa ja tekemään ajatuksesta erottaa liiketoimintalogiikka mikropalveluiksi houkuttelevammaksi. Tämän saavuttamiseksi tehtiin paljon työtä, luotiin erittäin "kevyt" viitekehys liiketoimintaprosesseille sekä palvelukomponentit tunnistettujen ongelmien ratkaisemiseksi monenlaisten sovellusongelmien yhteydessä. Haluamme jakaa nämä tulokset ja tehdä yhteisten komponenttien kehittämisestä avoimen pääsyn ilmaisella lisenssillä. Tämä vaatii jonkin verran vaivaa ja aikaa. Tällaisten ratkaisujen kysynnän ymmärtäminen voisi olla meille lisäkannustin. Ehdotetussa artikkelissa itse kehyksen ominaisuuksiin kiinnitetään vain vähän huomiota, mutta osa niistä näkyy esitetyistä esimerkeistä. Jos julkaisemme puitteemme, sille omistetaan erillinen artikkeli. Sillä välin olisimme kiitollisia, jos jättäisit hieman palautetta vastaamalla kysymykseen:

Vain rekisteröityneet käyttäjät voivat osallistua kyselyyn. Kirjaudu sisään, ole kiltti.

Tarvitsetko toisen viitekehyksen liiketoimintaprosesseille?

  • 18,8%Kyllä, olen etsinyt jotain tällaista jo pitkään

  • 12,5%Olen kiinnostunut kuulemaan lisää toteutuksestasi, siitä voi olla hyötyä2

  • 6,2%Käytämme yhtä olemassa olevista kehyksistä, mutta harkitsemme vaihtamista1

  • 18,8%Käytämme yhtä olemassa olevista kehyksistä, kaikki on kunnossa3

  • 18,8%pärjäämme ilman kehystä3

  • 25,0%kirjoita omasi 4

16 käyttäjää äänesti. 7 käyttäjää pidättyi äänestämästä.

Lähde: will.com

Lisää kommentti