Integrarea stilului BPM

Integrarea stilului BPM

Bună ziua, Habr!

Compania noastră este specializată în dezvoltarea de soluții software de clasă ERP, în care partea leului este ocupată de sisteme tranzacționale cu o cantitate imensă de logică de afaceri și flux de lucru la EDMS. Versiunile moderne ale produselor noastre se bazează pe tehnologii JavaEE, dar experimentăm în mod activ și microservicii. Una dintre cele mai problematice domenii ale unor astfel de soluții este integrarea diferitelor subsisteme legate de domenii adiacente. Sarcinile de integrare ne-au dat întotdeauna o mare bătaie de cap, indiferent de stilurile arhitecturale, stivele de tehnologie și cadrele pe care le folosim, dar recent s-au înregistrat progrese în rezolvarea unor astfel de probleme.

În articolul adus în atenție, voi vorbi despre experiența și cercetarea arhitecturală a NPO Krista în zona desemnată. Vom lua în considerare și un exemplu de soluție simplă a unei probleme de integrare din punctul de vedere al unui dezvoltator de aplicații și vom afla ce se ascunde în spatele acestei simplități.

Disclaimer

Soluțiile arhitecturale și tehnice descrise în articol sunt oferite de mine pe baza experienței personale în contextul unor sarcini specifice. Aceste soluții nu pretind a fi universale și pot să nu fie optime în alte condiții de utilizare.

Ce legătură are BPM-ul cu el?

Pentru a răspunde la această întrebare, trebuie să ne aprofundăm puțin în specificul problemelor aplicate ale soluțiilor noastre. Partea principală a logicii de afaceri în sistemul nostru tranzacțional tipic este introducerea datelor în baza de date prin interfețe cu utilizatorul, verificarea manuală și automată a acestor date, trecerea lor printr-un flux de lucru, publicarea lor într-un alt sistem/bază de date analitică/arhivă, generarea de rapoarte. Astfel, funcția cheie a sistemului pentru clienți este automatizarea proceselor interne de afaceri ale acestora.

Pentru comoditate, folosim termenul „document” în comunicare ca o abstractizare a unui set de date, unite printr-o cheie comună, la care poate fi „atașat” un anumit flux de lucru.
Dar cum rămâne cu logica integrării? La urma urmei, sarcina de integrare este generată de arhitectura sistemului, care este „ferăstrău” în părți NU la cererea clientului, ci sub influența unor factori complet diferiți:

  • sub influența legii lui Conway;
  • ca urmare a reutilizarii subsistemelor dezvoltate anterior pentru alte produse;
  • așa cum a decis arhitectul, pe baza cerințelor nefuncționale.

Există o mare tentație de a separa logica de integrare de logica de afaceri a fluxului de lucru principal, pentru a nu polua logica de afaceri cu artefacte de integrare și pentru a salva dezvoltatorul de aplicații de a trebui să se adâncească în particularitățile peisajului arhitectural al sistemului. Această abordare are o serie de avantaje, dar practica arată ineficiența sa:

  • rezolvarea problemelor de integrare alunecă de obicei către cele mai simple opțiuni sub formă de apeluri sincrone din cauza punctelor de extensie limitate în implementarea fluxului de lucru principal (mai multe despre deficiențele integrării sincrone mai jos);
  • artefactele de integrare încă pătrund în logica principală de afaceri atunci când este necesar feedback de la un alt subsistem;
  • dezvoltatorul aplicației ignoră integrarea și o poate rupe cu ușurință prin schimbarea fluxului de lucru;
  • sistemul încetează să mai fie un singur întreg din punctul de vedere al utilizatorului, „cusăturile” dintre subsisteme devin vizibile, apar operațiuni redundante ale utilizatorului care inițiază transferul de date de la un subsistem la altul.

O altă abordare este să luați în considerare interacțiunile de integrare ca parte integrantă a logicii de afaceri de bază și a fluxului de lucru. Pentru a împiedica creșterea vertiginoasă a cerințelor de competențe ale dezvoltatorilor de aplicații, crearea de noi interacțiuni de integrare ar trebui să se facă ușor și natural, cu opțiuni minime pentru alegerea unei soluții. Acest lucru este mai dificil decât pare: instrumentul trebuie să fie suficient de puternic pentru a oferi utilizatorului varietatea necesară de opțiuni pentru utilizarea sa și, în același timp, să nu permită să fie împușcat în picior. Există multe întrebări la care un inginer trebuie să răspundă în contextul sarcinilor de integrare, dar la care un dezvoltator de aplicații nu ar trebui să se gândească în munca de zi cu zi: limitele tranzacției, consistența, atomicitatea, securitatea, scalarea, distribuirea încărcării și a resurselor, rutare, marshaling, contexte de propagare și comutare etc. Este necesar să oferim dezvoltatorilor de aplicații șabloane de decizie destul de simple, în care răspunsurile la toate astfel de întrebări sunt deja ascunse. Aceste tipare ar trebui să fie suficient de sigure: logica afacerii se schimbă foarte des, ceea ce crește riscul introducerii de erori, costul erorilor ar trebui să rămână la un nivel destul de scăzut.

Dar totuși, ce legătură are BPM-ul cu el? Există multe opțiuni pentru implementarea fluxului de lucru...
Într-adevăr, o altă implementare a proceselor de afaceri este foarte populară în soluțiile noastre - prin setarea declarativă a diagramei de tranziție a stării și conectarea handler-urilor cu logica de afaceri la tranziții. În același timp, starea care determină poziția actuală a „documentului” în procesul de afaceri este un atribut al „documentului” însuși.

Integrarea stilului BPM
Așa arată procesul la începutul proiectului

Popularitatea unei astfel de implementări se datorează simplității și vitezei relative de creare a proceselor de afaceri liniare. Cu toate acestea, pe măsură ce sistemele software devin mai complexe, partea automată a procesului de afaceri crește și devine mai complexă. Este nevoie de descompunere, reutilizare a părților proceselor, precum și de procese de bifurcare, astfel încât fiecare ramură să fie executată în paralel. În astfel de condiții, instrumentul devine incomod, iar diagrama de tranziție a stărilor își pierde conținutul informațional (interacțiunile de integrare nu se reflectă deloc în diagramă).

Integrarea stilului BPM
Așa arată procesul după mai multe iterații de clarificare a cerințelor

Calea de ieșire din această situație a fost integrarea motorului jBPM în unele produse cu cele mai complexe procese de afaceri. Pe termen scurt, această soluție a avut un oarecare succes: a devenit posibilă implementarea proceselor de afaceri complexe, menținând în același timp o diagramă destul de informativă și actualizată în notație BPMN2.

Integrarea stilului BPM
O mică parte a unui proces complex de afaceri

Pe termen lung, soluția nu s-a ridicat la înălțimea așteptărilor: intensitatea mare a forței de muncă a creării proceselor de afaceri prin instrumente vizuale nu a permis atingerea unor indicatori de productivitate acceptabili, iar instrumentul în sine a devenit unul dintre cele mai antipatice în rândul dezvoltatorilor. Au existat, de asemenea, plângeri cu privire la structura internă a motorului, ceea ce a dus la apariția multor „petice” și „cârje”.

Principalul aspect pozitiv al utilizării jBPM a fost realizarea beneficiilor și daunelor de a avea propria stare persistentă pentru o instanță de proces de afaceri. Am văzut, de asemenea, posibilitatea utilizării unei abordări de proces pentru a implementa protocoale complexe de integrare între diferite aplicații folosind interacțiuni asincrone prin semnale și mesaje. Prezența unei stări persistente joacă un rol crucial în acest sens.

Pe baza celor de mai sus, putem concluziona: Abordarea procesului în stilul BPM ne permite să rezolvăm o gamă largă de sarcini pentru automatizarea proceselor de afaceri din ce în ce mai complexe, să integrăm armonios activitățile de integrare în aceste procese și să păstrăm capacitatea de a afișa vizual procesul implementat într-o notație adecvată.

Dezavantajele apelurilor sincrone ca model de integrare

Integrarea sincronă se referă la cel mai simplu apel de blocare. Un subsistem acționează ca partea de server și expune API-ul cu metoda dorită. Un alt subsistem acționează ca o parte a clientului și, la momentul potrivit, efectuează un apel cu așteptarea unui rezultat. În funcție de arhitectura sistemului, părțile client și server pot fi găzduite fie în aceeași aplicație și proces, fie în altele diferite. În cel de-al doilea caz, trebuie să aplicați o anumită implementare a RPC și să furnizați o distribuție a parametrilor și a rezultatului apelului.

Integrarea stilului BPM

Un astfel de model de integrare are un set destul de mare de dezavantaje, dar este utilizat pe scară largă în practică datorită simplității sale. Viteza de implementare captivează și te face să o aplici iar și iar în condițiile „arderii” termenelor, scriind soluția în datorie tehnică. Dar se întâmplă și ca dezvoltatorii fără experiență să-l folosească inconștient, pur și simplu fără să-și dea seama de consecințele negative.

Pe lângă creșterea cea mai evidentă a conectivității subsistemelor, există probleme mai puțin evidente cu tranzacțiile de „împrăștiere” și „întindere”. Într-adevăr, dacă logica de afaceri face modificări, atunci tranzacțiile sunt indispensabile, iar tranzacțiile, la rândul lor, blochează anumite resurse ale aplicației afectate de aceste modificări. Adică, până când un subsistem nu așteaptă un răspuns de la altul, nu va putea finaliza tranzacția și elibera blocările. Acest lucru crește semnificativ riscul unei varietăți de efecte:

  • capacitatea de răspuns a sistemului este pierdută, utilizatorii așteaptă mult timp pentru răspunsuri la solicitări;
  • serverul nu mai răspunde în general la solicitările utilizatorilor din cauza unui pool de fire de execuție depășit: majoritatea firelor de execuție „stau” pe blocarea resursei ocupate de tranzacție;
  • încep să apară blocaje: probabilitatea apariției lor depinde în mare măsură de durata tranzacțiilor, de cantitatea de logica de afaceri și de blocările implicate în tranzacție;
  • apar erori de expirare a timpului de expirare a tranzacției;
  • serverul „cade” pe OutOfMemory dacă sarcina necesită procesarea și modificarea unor cantități mari de date, iar prezența integrărilor sincrone face foarte dificilă împărțirea procesării în tranzacții „mai ușoare”.

Din punct de vedere arhitectural, utilizarea apelurilor de blocare în timpul integrării duce la o pierdere a controlului calității subsistemelor individuale: este imposibil să se asigure obiectivele de calitate ale unui subsistem izolat de obiectivele de calitate ale altui subsistem. Dacă subsistemele sunt dezvoltate de diferite echipe, aceasta este o mare problemă.

Lucrurile devin și mai interesante dacă subsistemele care sunt integrate sunt în aplicații diferite și trebuie făcute modificări sincrone de ambele părți. Cum să faci aceste modificări tranzacționale?

Dacă se fac modificări în tranzacții separate, atunci va trebui furnizată o gestionare robustă a excepțiilor și o compensare, iar acest lucru elimină complet principalul avantaj al integrărilor sincrone - simplitatea.

Ne vin în minte și tranzacțiile distribuite, dar nu le folosim în soluțiile noastre: este greu de asigurat fiabilitatea.

„Saga” ca soluție la problema tranzacțiilor

Odată cu popularitatea tot mai mare a microserviciilor, există o cerere tot mai mare pentru Model Saga.

Acest model rezolvă perfect problemele de mai sus ale tranzacțiilor lungi și, de asemenea, extinde posibilitățile de gestionare a stării sistemului din partea logicii de afaceri: compensarea după o tranzacție nereușită poate să nu dea înapoi sistemul la starea inițială, ci să ofere o alternativă. ruta de prelucrare a datelor. De asemenea, vă permite să nu repetați pașii de prelucrare a datelor finalizați cu succes atunci când încercați să duceți procesul la un final „bun”.

Interesant este că în sistemele monolitice, acest model este relevant și atunci când vine vorba de integrarea subsistemelor slab cuplate și există efecte negative cauzate de tranzacțiile lungi și de blocările corespunzătoare de resurse.

În ceea ce privește procesele noastre de afaceri în stilul BPM, se dovedește a fi foarte ușor de implementat Sagas-urile: pașii individuali ai Sagas-urilor pot fi setate ca activități în cadrul procesului de afaceri, iar starea persistentă a procesului de afaceri determină, printre alte lucruri, starea internă a Saga. Adică nu avem nevoie de niciun mecanism suplimentar de coordonare. Tot ce aveți nevoie este un broker de mesaje cu suport pentru garanții „cel puțin o dată” ca transport.

Dar o astfel de soluție are și propriul „preț”:

  • logica de afaceri devine mai complexă: trebuie să stabiliți compensații;
  • va fi necesar să se abandoneze consistența deplină, care poate fi deosebit de sensibilă pentru sistemele monolitice;
  • arhitectura devine puțin mai complicată, este nevoie suplimentară de un broker de mesaje;
  • vor fi necesare instrumente suplimentare de monitorizare și administrare (deși în general acest lucru este chiar bun: calitatea serviciului sistemului va crește).

Pentru sistemele monolitice, justificarea utilizării „Sags” nu este atât de evidentă. Pentru microservicii și alte SOA, unde, cel mai probabil, există deja un broker, iar consistența deplină a fost sacrificată la începutul proiectului, beneficiile utilizării acestui model pot depăși semnificativ dezavantajele, mai ales dacă există un API convenabil la nivelul nivelul logicii de afaceri.

Încapsularea logicii de afaceri în microservicii

Când am început să experimentăm cu microservicii, a apărut o întrebare rezonabilă: unde să punem logica de afaceri a domeniului în relație cu serviciul care asigură persistența datelor de domeniu?

Când ne uităm la arhitectura diferitelor BPMS, poate părea rezonabil să separăm logica de afaceri de persistență: creați un strat de microservicii independente de platformă și domeniu, care formează mediul și containerul pentru executarea logicii de afaceri a domeniului și aranjați persistența datelor de domeniu ca o unitate separată. strat de microservicii foarte simple și ușoare. Procesele de afaceri în acest caz orchestrează serviciile stratului de persistență.

Integrarea stilului BPM

Această abordare are un avantaj foarte mare: puteți crește funcționalitatea platformei cât de mult doriți și doar stratul corespunzător de microservicii ale platformei se va „îngrășa” din asta. Procesele de afaceri din orice domeniu au imediat posibilitatea de a folosi noua funcționalitate a platformei de îndată ce aceasta este actualizată.

Un studiu mai detaliat a relevat deficiențe semnificative ale acestei abordări:

  • un serviciu de platformă care execută logica de afaceri a mai multor domenii simultan prezintă riscuri mari ca un singur punct de eșec. Schimbările frecvente ale logicii de afaceri cresc riscul ca erorile să conducă la defecțiuni la nivel de sistem;
  • probleme de performanță: logica de afaceri lucrează cu datele sale printr-o interfață îngustă și lentă:
    • datele vor fi din nou repartizate și pompate prin stiva de rețea;
    • serviciul de domeniu va returna adesea mai multe date decât necesită logica de afaceri pentru procesare, din cauza capacităților insuficiente de parametrizare a interogărilor la nivelul API-ului extern al serviciului;
    • mai multe bucăți independente de logică de afaceri pot solicita din nou aceleași date pentru procesare (puteți atenua această problemă adăugând bean-uri de sesiune care memorează datele în cache, dar acest lucru complică și mai mult arhitectura și creează probleme de prospețime a datelor și de invalidare a memoriei cache);
  • probleme tranzacționale:
    • procesele de afaceri cu stare persistentă stocate de serviciul platformei sunt incompatibile cu datele de domeniu și nu există modalități ușoare de a rezolva această problemă;
    • mutarea blocării datelor de domeniu din tranzacție: dacă logica de afaceri a domeniului trebuie să facă modificări, după verificarea mai întâi a corectitudinii datelor reale, este necesar să se excludă posibilitatea unei modificări competitive a datelor prelucrate. Blocarea externă a datelor poate ajuta la rezolvarea problemei, dar o astfel de soluție implică riscuri suplimentare și reduce fiabilitatea generală a sistemului;
  • complicații suplimentare la actualizare: în unele cazuri, trebuie să actualizați serviciul de persistență și logica de afaceri în mod sincron sau într-o secvență strictă.

În cele din urmă, a trebuit să mă întorc la elementele de bază: să încapsulez datele de domeniu și logica de afaceri a domeniului într-un singur microserviciu. Această abordare simplifică percepția microserviciului ca o componentă integrală a sistemului și nu dă naștere problemelor de mai sus. Nici acesta nu este gratuit:

  • Standardizarea API este necesară pentru interacțiunea cu logica de afaceri (în special, pentru a oferi activități utilizatorilor ca parte a proceselor de afaceri) și serviciile platformei API; o atenție mai atentă la modificările API, este necesară compatibilitatea înainte și înapoi;
  • este necesar să se adauge biblioteci de rulare suplimentare pentru a asigura funcționarea logicii de business ca parte a fiecărui astfel de microserviciu, iar acest lucru dă naștere la noi cerințe pentru astfel de biblioteci: ușurință și un minim de dependențe tranzitive;
  • Dezvoltatorii de logică de afaceri trebuie să țină evidența versiunilor bibliotecii: dacă un microserviciu nu a fost finalizat de mult timp, atunci cel mai probabil va conține o versiune învechită a bibliotecilor. Acesta poate fi un obstacol neașteptat pentru adăugarea unei noi caracteristici și poate necesita migrarea vechii logici de afaceri a unui astfel de serviciu către versiuni noi ale bibliotecilor dacă au existat modificări incompatibile între versiuni.

Integrarea stilului BPM

Într-o astfel de arhitectură este prezent și un strat de servicii de platformă, dar acest strat nu mai formează un container pentru executarea logicii de afaceri a domeniului, ci doar mediul acestuia, oferind funcții auxiliare de „platformă”. Un astfel de strat este necesar nu numai pentru a menține ușurința microserviciilor de domeniu, ci și pentru a centraliza managementul.

De exemplu, activitățile utilizatorilor în procesele de afaceri generează sarcini. Cu toate acestea, atunci când lucrează cu sarcini, utilizatorul trebuie să vadă sarcinile din toate domeniile din lista generală, ceea ce înseamnă că trebuie să existe un serviciu adecvat de platformă de înregistrare a sarcinilor, fără logica de afaceri a domeniului. Păstrarea încapsulării logicii de afaceri în acest context este destul de problematică, iar acesta este un alt compromis al acestei arhitecturi.

Integrarea proceselor de afaceri prin ochii unui dezvoltator de aplicații

După cum sa menționat deja mai sus, dezvoltatorul de aplicații trebuie să fie abstras de caracteristicile tehnice și inginerie ale implementării interacțiunii mai multor aplicații pentru a putea conta pe o bună productivitate a dezvoltării.

Să încercăm să rezolvăm o problemă de integrare destul de dificilă, inventată special pentru articol. Aceasta va fi o sarcină de „joc” care implică trei aplicații, unde fiecare dintre ele definește un nume de domeniu: „app1”, „app2”, „app3”.

În interiorul fiecărei aplicații sunt lansate procese de business care încep să „joace mingea” prin magistrala de integrare. Mesajele numite „Minge” vor acționa ca minge.

Regulile jocului:

  • primul jucător este inițiatorul. El invită alți jucători la joc, începe jocul și îl poate încheia oricând;
  • alți jucători își declară participarea la joc, „se familiarizează” între ei și cu primul jucător;
  • după ce a primit mingea, jucătorul alege un alt jucător participant și îi pasează mingea. Se contorizează numărul total de treceri;
  • fiecare jucător are „energie”, care scade cu fiecare pasă a mingii de către acel jucător. Când energia se epuizează, jucătorul este eliminat din joc, anunțându-și retragerea;
  • dacă jucătorul este lăsat singur, acesta își declară imediat plecarea;
  • când toți jucătorii sunt eliminați, primul jucător declară sfârșitul jocului. Dacă a părăsit jocul mai devreme, atunci rămâne de urmat jocul pentru a-l finaliza.

Pentru a rezolva această problemă, voi folosi DSL-ul nostru pentru procesele de afaceri, ceea ce vă permite să descrieți logica în Kotlin în mod compact, cu un minim de o boilerplate.

În aplicația app1, procesul de afaceri al primului jucător (el este și inițiatorul jocului) va funcționa:

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

Pe lângă executarea logicii de afaceri, codul de mai sus poate produce un model obiect al unui proces de afaceri care poate fi vizualizat ca o diagramă. Nu am implementat încă vizualizatorul, așa că a trebuit să petrecem puțin timp desenând (aici am simplificat puțin notația BPMN cu privire la utilizarea porților pentru a îmbunătăți consistența diagramei cu codul de mai sus):

Integrarea stilului BPM

app2 va include procesul de afaceri al unui alt jucător:

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

Diagramă:

Integrarea stilului BPM

În aplicația app3, vom face jucătorul cu un comportament ușor diferit: în loc să aleagă aleatoriu următorul jucător, el va acționa conform algoritmului round-robin:

clasa RoundRobinPlayer

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

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

class PlayersList: ArrayList<PlayerInfo>()

class RoundRobinPlayer : ProcessImpl<RoundRobinPlayer>(roundRobinPlayerModel) {

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

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

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

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

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

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

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

În caz contrar, comportamentul jucătorului nu diferă de cel anterior, așa că diagrama nu se modifică.

Acum avem nevoie de un test pentru a rula totul. Voi da doar codul testului în sine, pentru a nu aglomera articolul cu un boilerplate (de fapt, am folosit mediul de testare creat mai devreme pentru a testa integrarea altor procese de afaceri):

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");
    // Теперь нужно немного подождать, пока игроки "познакомятся" друг с другом.
    // Ждать через 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;
}

Rulați testul, uitați-vă la jurnal:

ieșirea consolei

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

Din toate acestea se pot trage câteva concluzii importante:

  • dacă instrumentele necesare sunt disponibile, dezvoltatorii de aplicații pot crea interacțiuni de integrare între aplicații fără a se rupe de logica de afaceri;
  • complexitatea (complexitatea) unei sarcini de integrare care necesită competențe de inginerie poate fi ascunsă în cadrul cadrului dacă este stabilită inițial în arhitectura cadrului. Dificultatea sarcinii (dificultatea) nu poate fi ascunsă, astfel încât soluția unei sarcini dificile din cod va arăta în consecință;
  • la dezvoltarea logicii integrării, este necesar să se țină seama în cele din urmă de consecvența și lipsa de liniarizare a schimbării de stare a tuturor participanților la integrare. Acest lucru ne obligă să complicăm logica pentru a o face insensibilă la ordinea în care apar evenimentele externe. În exemplul nostru, jucătorul este forțat să ia parte la joc după ce își anunță ieșirea din joc: alți jucători vor continua să-i transmită mingea până când informațiile despre ieșirea lui ajung și sunt procesate de toți participanții. Această logică nu decurge din regulile jocului și este o soluție de compromis în cadrul arhitecturii alese.

În continuare, să vorbim despre diferitele subtilități ale soluției noastre, compromisuri și alte puncte.

Toate mesajele într-o singură coadă

Toate aplicațiile integrate funcționează cu o singură magistrală de integrare, care este prezentată ca un broker extern, un BPMQueue pentru mesaje și un subiect BPMTopic pentru semnale (evenimente). Trecerea tuturor mesajelor printr-o singură coadă este în sine un compromis. La nivel de logica de afaceri, acum puteți introduce câte tipuri noi de mesaje doriți fără a face modificări structurii sistemului. Aceasta este o simplificare semnificativă, dar implică anumite riscuri, care, în contextul sarcinilor noastre tipice, ni s-au părut nu atât de semnificative.

Integrarea stilului BPM

Totuși, există o subtilitate aici: fiecare aplicație își filtrează mesajele din coada de la intrare, după numele domeniului său. De asemenea, domeniul poate fi specificat în semnale, dacă trebuie să limitați „sfera” semnalului la o singură aplicație. Aceasta ar trebui să mărească lățimea de bandă a magistralei, dar logica de afaceri trebuie să funcționeze acum cu nume de domenii: obligatoriu pentru adresarea mesajelor, de dorit pentru semnale.

Asigurarea fiabilității magistralei de integrare

Fiabilitatea este alcătuită din mai multe lucruri:

  • Brokerul de mesaje ales este o componentă critică a arhitecturii și un singur punct de eșec: trebuie să fie suficient de tolerant la erori. Ar trebui să utilizați numai implementări testate în timp, cu un suport bun și o comunitate mare;
  • este necesar să se asigure o disponibilitate ridicată a brokerului de mesaje, pentru care acesta trebuie separat fizic de aplicațiile integrate (disponibilitatea ridicată a aplicațiilor cu logica de business aplicată este mult mai dificil și mai costisitor de furnizat);
  • brokerul este obligat să ofere „cel puțin o dată” garanții de livrare. Aceasta este o cerință obligatorie pentru funcționarea fiabilă a magistralei de integrare. Nu este nevoie de garanții de nivel „exact o dată”: procesele de afaceri nu sunt de obicei sensibile la sosirea repetată a mesajelor sau evenimentelor, iar în sarcinile speciale în care acest lucru este important, este mai ușor să adăugați verificări suplimentare la logica afacerii decât să utilizați în mod constant mai degrabă „costisitoare” „garanții;
  • trimiterea de mesaje și semnale trebuie să fie implicată într-o tranzacție comună cu o schimbare a stării proceselor de afaceri și a datelor de domeniu. Opțiunea preferată ar fi să folosiți modelul Căsuță de ieșire tranzacțională, dar va necesita un tabel suplimentar în baza de date și un releu. În aplicațiile JEE, acest lucru poate fi simplificat prin utilizarea unui manager JTA local, dar conexiunea la brokerul selectat trebuie să poată funcționa în modul XA;
  • manipulatorii de mesaje și evenimente primite trebuie, de asemenea, să lucreze cu tranzacția de modificare a stării procesului de afaceri: dacă o astfel de tranzacție este anulată, atunci și primirea mesajului trebuie să fie anulată;
  • mesajele care nu au putut fi livrate din cauza erorilor ar trebui să fie stocate într-un magazin separat D.L.Q. (Coada de scrisori moarte). Pentru a face acest lucru, am creat un microserviciu de platformă separat care stochează astfel de mesaje în stocarea sa, le indexează după atribute (pentru grupare și căutare rapidă) și expune API-ul pentru vizualizare, retrimitere la adresa de destinație și ștergere a mesajelor. Administratorii de sistem pot lucra cu acest serviciu prin interfața lor web;
  • în setările brokerului, trebuie să ajustați numărul de reîncercări de livrare și întârzierile dintre livrări pentru a reduce probabilitatea ca mesajele să intre în DLQ (este aproape imposibil să calculați parametrii optimi, dar puteți acționa empiric și îi puteți ajusta în timpul Operațiune);
  • magazinul DLQ ar trebui monitorizat continuu, iar sistemul de monitorizare ar trebui să notifice administratorii de sistem, astfel încât aceștia să poată răspunde cât mai repede posibil atunci când apar mesaje nelivrate. Acest lucru va reduce „zona de deteriorare” a unei erori sau a unei erori de logica de afaceri;
  • magistrala de integrare trebuie să fie insensibilă la absența temporară a aplicațiilor: abonamentele la subiecte trebuie să fie durabile, iar numele de domeniu al aplicației trebuie să fie unic pentru ca altcineva să nu încerce să-și proceseze mesajul din coadă în timpul absenței aplicației.

Asigurarea siguranței firelor de logica de afaceri

Aceeași instanță a unui proces de afaceri poate primi mai multe mesaje și evenimente simultan, a căror procesare va începe în paralel. În același timp, pentru un dezvoltator de aplicații, totul ar trebui să fie simplu și sigur pentru fire.

Logica de afaceri proces procesează fiecare eveniment extern care afectează acest proces de afaceri în mod individual. Aceste evenimente pot fi:

  • lansarea unei instanțe de proces de afaceri;
  • o acțiune a utilizatorului legată de o activitate în cadrul unui proces de afaceri;
  • primirea unui mesaj sau semnal la care este abonată o instanță de proces de afaceri;
  • expirarea temporizatorului setat de instanța procesului de afaceri;
  • acțiune de control prin API (de exemplu, anularea procesului).

Fiecare astfel de eveniment poate schimba starea unei instanțe de proces de afaceri: unele activități se pot termina, iar altele pot începe, valorile proprietăților persistente se pot schimba. Închiderea oricărei activități poate duce la activarea uneia sau mai multor dintre următoarele activități. Aceștia, la rândul lor, pot înceta să aștepte alte evenimente sau, dacă nu au nevoie de date suplimentare, pot finaliza în aceeași tranzacție. Înainte de închiderea tranzacției, noua stare a procesului de afaceri este stocată în baza de date, unde va aștepta următorul eveniment extern.

Datele persistente ale procesului de afaceri stocate într-o bază de date relațională reprezintă un punct de sincronizare a procesării foarte convenabil atunci când se utilizează SELECT FOR UPDATE. Dacă o tranzacție a reușit să obțină starea procesului de afaceri din baza de date pentru a o modifica, atunci nicio altă tranzacție în paralel nu va putea obține aceeași stare pentru o altă modificare, iar după finalizarea primei tranzacții, a doua este garantat că va primi starea deja schimbată.

Folosind încuietori pesimiste pe partea DBMS, îndeplinim toate cerințele necesare ACIDși păstrează, de asemenea, capacitatea de a scala aplicația cu logica de afaceri prin creșterea numărului de instanțe care rulează.

Cu toate acestea, blocajele pesimiste ne amenință cu blocaje, ceea ce înseamnă că SELECT FOR UPDATE ar trebui să se limiteze în continuare la un timeout rezonabil în cazul blocajelor în unele cazuri flagrante din logica de afaceri.

O altă problemă este sincronizarea începerii procesului de afaceri. Deși nu există nicio instanță de proces de afaceri, nu există nicio stare în baza de date, așa că metoda descrisă nu va funcționa. Dacă doriți să asigurați unicitatea unei instanțe de proces de afaceri într-un anumit domeniu, atunci aveți nevoie de un fel de obiect de sincronizare asociat cu clasa de proces și domeniul corespunzător. Pentru a rezolva această problemă, folosim un mecanism de blocare diferit care ne permite să blocăm o resursă arbitrară specificată de o cheie în format URI printr-un serviciu extern.

În exemplele noastre, procesul de afaceri InitialPlayer conține o declarație

uniqueConstraint = UniqueConstraints.singleton

Prin urmare, jurnalul conține mesaje despre luarea și eliberarea blocării cheii corespunzătoare. Nu există astfel de mesaje pentru alte procese de afaceri: uniqueConstraint nu este setat.

Probleme de proces de afaceri cu stare persistentă

Uneori, a avea o stare persistentă nu numai că ajută, dar și împiedică cu adevărat dezvoltarea.
Problemele încep atunci când trebuie să faceți modificări logicii de afaceri și/sau modelului de proces de afaceri. Nicio astfel de modificare nu este compatibilă cu starea veche a proceselor de afaceri. Dacă există multe instanțe „live” în baza de date, atunci efectuarea de modificări incompatibile poate cauza o mulțime de probleme, pe care le-am întâlnit adesea când folosim jBPM.

În funcție de adâncimea schimbării, puteți acționa în două moduri:

  1. creați un nou tip de proces de afaceri pentru a nu face modificări incompatibile celui vechi și utilizați-l în locul celui vechi atunci când porniți instanțe noi. Instanțele vechi vor continua să funcționeze „în mod vechi”;
  2. migrați starea persistentă a proceselor de afaceri atunci când actualizați logica de afaceri.

Prima modalitate este mai simplă, dar are limitările și dezavantajele sale, de exemplu:

  • duplicarea logicii de afaceri în multe modele de procese de afaceri, o creștere a volumului logicii de afaceri;
  • adesea este necesară o tranziție instantanee la o nouă logică de business (aproape întotdeauna în ceea ce privește sarcinile de integrare);
  • dezvoltatorul nu știe în ce moment este posibil să ștergeți modele învechite.

În practică, folosim ambele abordări, dar am luat o serie de decizii pentru a ne simplifica viața:

  • în baza de date, starea persistentă a procesului de afaceri este stocată într-o formă ușor de citit și ușor de procesat: într-un șir în format JSON. Acest lucru vă permite să efectuați migrări atât în ​​interiorul aplicației, cât și în exterior. În cazuri extreme, îl puteți modifica și cu mânere (util mai ales în dezvoltare în timpul depanării);
  • logica de afaceri de integrare nu folosește numele proceselor de afaceri, astfel încât în ​​orice moment este posibilă înlocuirea implementării unuia dintre procesele participante cu unul nou, cu un nume nou (de exemplu, „InitialPlayerV2”). Legarea are loc prin numele mesajelor și semnalelor;
  • modelul de proces are un număr de versiune, pe care îl incrementăm dacă facem modificări incompatibile acestui model, iar acest număr este stocat împreună cu starea instanței procesului;
  • starea persistentă a procesului este citită mai întâi de la bază într-un model de obiect convenabil cu care procedura de migrare poate funcționa dacă numărul versiunii modelului s-a schimbat;
  • procedura de migrare este plasată lângă logica de business și se numește „leneș” pentru fiecare instanță a procesului de business la momentul restaurării acestuia din baza de date;
  • dacă trebuie să migrați starea tuturor instanțelor de proces rapid și sincron, se folosesc soluții mai clasice de migrare a bazei de date, dar trebuie să lucrați cu JSON acolo.

Am nevoie de un alt cadru pentru procesele de afaceri?

Soluțiile descrise în articol ne-au permis să ne simplificăm semnificativ viața, să extindem gama de probleme rezolvate la nivel de dezvoltare a aplicațiilor și să facem mai atractivă ideea de a separa logica de afaceri în microservicii. Pentru aceasta, s-a depus multă muncă, a fost creat un cadru foarte „ușor” pentru procesele de afaceri, precum și componente de servicii pentru rezolvarea problemelor identificate în contextul unei game largi de sarcini aplicate. Avem dorința de a împărtăși aceste rezultate, de a aduce dezvoltarea componentelor comune în acces deschis sub o licență gratuită. Acest lucru va necesita ceva efort și timp. Înțelegerea cererii pentru astfel de soluții ar putea fi un stimulent suplimentar pentru noi. În articolul propus, se acordă foarte puțină atenție capacităților cadrului în sine, dar unele dintre ele sunt vizibile din exemplele prezentate. Dacă totuși publicăm cadrul nostru, un articol separat îi va fi dedicat. Între timp, vă vom fi recunoscători dacă lăsați un mic feedback răspunzând la întrebarea:

Numai utilizatorii înregistrați pot participa la sondaj. Loghează-te, Vă rog.

Am nevoie de un alt cadru pentru procesele de afaceri?

  • 18,8%Da, de mult caut asa ceva.

  • 12,5%este interesant să aflați mai multe despre implementarea dvs., poate fi util2

  • 6,2%folosim unul dintre cadrele existente, dar ne gândim să-l înlocuim1

  • 18,8%folosim unul dintre cadrele existente, totul se potrivește3

  • 18,8%face față fără cadru3

  • 25,0%scrie pe al tău4

Au votat 16 utilizatori. 7 utilizatori s-au abținut.

Sursa: www.habr.com

Adauga un comentariu