
Bună, Habr!
Compania noastră se specializează în dezvoltarea de soluții software de tip ERP, în cadrul cărora o pondere semnificativă o au sistemele tranzacționale cu un volum masiv de logică de afaceri și flux de documente de tip SED. Versiunile moderne ale produselor noastre se bazează pe tehnologiile JavaEE, dar experimentăm de asemenea activ cu microserviciile. Unul dintre cele mai problematice aspecte ale acestor soluții este integrarea diverselor subsisteme care aparțin domeniilor învecinate. Sarcinile de integrare ne-au cauzat întotdeauna dureri de cap imense, indiferent de stilurile arhitecturale, stivele tehnologice și cadrul utilizat, însă recent s-a observat un progres în soluționarea acestor provocări.
În articolul pe care vi-l prezentăm, voi discuta experiența și cercetările arhitecturale ale NPO „Krista” în domeniul respectiv. De asemenea, vom analiza un exemplu de soluționare simplă a unei sarcini de integrare din perspectiva unui dezvoltator de aplicații și vom descoperi ce se ascunde în spatele acestei simplități.
Declinarea responsabilității
Soluțiile arhitecturale și tehnice descrise în articol sunt propuse de mine pe baza experienței personale în contextul sarcinilor specifice. Aceste soluții nu pretind a fi universale și pot să nu fie optime în alte condiții de utilizare.
Și ce are de-a face BPM-ul cu asta?
Pentru a răspunde la această întrebare, trebuie să ne adâncim puțin în specificul sarcinilor aplicațiilor noastre. Partea principală a logicii de afaceri în sistemul nostru tranzacțional tipic constă în introducerea de date în Baza de Date prin intermediul interfețelor utilizatorului, verificarea manuală și automatizată a acestor date, desfășurarea lor într-un anumit flux de lucru, publicarea într-un alt sistem / bază de date analitică / arhivă, generarea de rapoarte. Astfel, funcția cheie a sistemului pentru clienți este automatizarea proceselor lor interne de afaceri.
Pentru comoditate, folosim în comunicare termenul „document” ca o anumită abstracție a unui set de date unite printr-o cheie comună, la care se poate „atașa” un anumit flux de lucru.
Dar cum rămâne cu logica de integrare? Sarcina de integrare este generată de arhitectura sistemului, care este „separată” î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 reutilizării subsistemelor, dezvoltate anterior pentru alte produse;
- la decizia arhitectului, 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 contamina logica de afaceri cu artefacte de integrare și pentru a scuti dezvoltatorul de aplicații de necesitatea de a înțelege specificitățile peisajului arhitectural al sistemului. Această abordare are o serie de avantaje, însă practica arată ineficiența sa:
- rezolvarea problemelor de integrare se transformă de obicei în cele mai simple variante sub formă de apeluri sincrone, din cauza limitării punctelor de extindere în implementarea fluxului de lucru principal (despre dezavantajele integrării sincrone — mai jos);
- artefactele de integrare pătrund în continuare în logica de afaceri principală, atunci când este necesar feedback dintr-o altă subsistem;
- dezvoltatorul de aplicații ignoră integrarea și o poate rupe cu ușurință, modificând fluxul de lucru;
- sistemul încetează să mai fie un întreg din perspectiva utilizatorului, devin vizibile "cusăturile" între subsisteme, apar operațiuni de utilizator redundante care inițiază transferul de date dintr-o subsistemă în alta.
O altă abordare constă în considerarea interacțiunilor de integrare ca o parte integrantă a logicii de business principale și a fluxului de lucru. Pentru a evita ca cerințele pentru calificarea dezvoltatorilor aplicațiilor să crească exagerat, crearea de noi interacțiuni de integrare ar trebui să fie realizată ușor și fără efort, cu posibile opțiuni minime pentru alegerea modului de soluționare. Acest lucru este mai complicat decât pare: instrumentul trebuie să fie suficient de puternic pentru a oferi utilizatorului un număr necesar de opțiuni de utilizare și în același timp să nu permită „să își tragă un glonț în picior”. Există multe întrebări la care inginerul trebuie să răspundă în contextul sarcinilor de integrare, dar la care dezvoltatorul de aplicații nu ar trebui să se gândească în munca sa zilnică: limitele tranzacțiilor, consistența, atomicitatea, securitatea, scalabilitatea, distribuția sarcinilor și resurselor, rutarea, marshallingul, propagarea și comutarea contextelor etc. Trebuie să oferim dezvoltatorilor aplicațiilor șabloane de soluții suficient de simple, în care să fie deja ascunse răspunsurile la toate aceste întrebări. Aceste șabloane trebuie să fie suficient de sigure: logica de business se schimbă foarte frecvent, ceea ce crește riscurile de erori, iar costul erorilor trebuie să rămână la un nivel suficient de scăzut.
Dar ce legătură are BPM cu asta? Există atâtea variante de implementare a fluxului de lucru...
Într-adevăr, în soluțiile noastre o altă implementare a proceselor de business este foarte populară – prin definirea declarativă a diagramelor de tranziții de stare și conectarea handlerelor cu logica de business pe tranziții. În acest caz, starea care definește poziția curentă a „documentului” în procesul de business este un atribut al „documentului” în sine.

Așa arată procesul la începutul proiectului.
Popularitatea unei astfel de implementări se datorează relativității simplității și vitezei de creare a proceselor de afaceri liniare. Totuși, pe măsură ce sistemele software devin din ce în ce mai complexe, partea automatizată a procesului de afaceri se extinde și devine mai complicată. Există o necesitate de descompunere, reutilizare a unor părți ale proceselor, precum și de ramificare a proceselor, astfel încât fiecare ramură să fie executată în paralel. În astfel de condiții, instrumentul devine incomod, iar diagrama tranzițiilor de stare își pierde informativitatea (interacțiunile de integrare nu sunt reflectate deloc în diagramă).

Asta este cum arată procesul după câteva iterații de clarificare a cerințelor.
Soluția la această situație a fost integrarea motorului în unele produse cu cele mai complexe procese de afaceri. Pe termen scurt, această soluție a avut un anumit succes: a apărut posibilitatea implementării unor procese de afaceri complexe, menținând în același timp o diagramă suficient de informativă și actuală în notația .

O mică parte dintr-un proces complex de afaceri.
Pe termen lung, soluția nu a justificat așteptările: efortul ridicat necesar pentru crearea proceselor de afaceri prin instrumente vizuale nu a permis atingerea unor indicatori de productivitate acceptabili, iar instrumentul a devenit unul dintre cele mai puțin preferate de către dezvoltatori. De asemenea, au existat nemulțumiri cu privire la structura internă a motorului, ceea ce a dus la apariția multor "patch-uri" și "soluții temporare".
Cel mai mare aspect pozitiv al aplicării jBPM a fost conștientizarea beneficiilor și dezavantajelor existenței unui stat persistent pentru instanța procesului de afaceri. De asemenea, am observat posibilitatea aplicării abordării procesuale pentru implementarea unor protocoale complexe de integrare între diferite aplicații, folosind interacțiuni asincrone prin semnale și mesaje. Existența unui stat persistent joacă un rol esențial în acest sens.
Pe baza celor spuse, se poate concluziona: abordarea procesuală în stil BPM ne permite să rezolvăm un spectru larg de sarcini de automatizare a proceselor de afaceri în continuă complexitate, integrând armonios activitățile de integrare în aceste procese și menținând posibilitatea unei reprezentări vizuale a procesului realizat în notația adecvată.
Dezavantajele apelurilor sincrone ca model de integrare
Prin integrarea sincrona se intelege un apel blocant simplu. O subsistemă joacă rolul de parte server și oferă un API cu metoda necesară. O altă subsistemă joacă rolul de parte client și, la momentul potrivit, efectuează apelul așteptând rezultatul. În funcție de arhitectura sistemului, părțile client și server pot fi plasate fie într-o aplicație și proces, fie în contexte separate. În acest din urmă caz, este necesară aplicarea unei implementări RPC și asigurarea marshalingului parametrilor și rezultatelor apelului.

Acest model de integrare are un set suficient de mare de dezavantaje, dar este folosit foarte larg în practică datorită simplității sale. Viteza de implementare este atrăgătoare și determină aplicarea sa repetată în condiții de termene "strânse", lăsând astfel soluția ca o datorie tehnică. Însă se întâmplă și ca dezvoltatorii neexperimentați să-l folosească fără să fie conștienți de consecințele negative.
Pe lângă creșterea evidentă a legăturii între subsisteme, există și probleme mai puțin evidente cu "împrăștierea" și "extinderea" tranzacțiilor. De fapt, dacă logica de business aduce vreo modificare, atunci nu se poate evita tranzacțiile, iar tranzacțiile, la rândul lor, blochează anumite resurse ale aplicației afectate de aceste modificări. Asta înseamnă că, până când o subsistemă nu primește răspuns de la cealaltă, nu va putea finaliza tranzacția și ridica blocajele. Aceasta crește semnificativ riscul apariției diverselor efecte:
- se pierde reactivitatea sistemului, utilizatorii așteaptă mult timp răspunsuri la solicitări;
- serverul încetează complet să răspundă la solicitările utilizatorilor din cauza pooling-ului de fire ocupat: majoritatea firelor sunt "blocate" pe resursa ocupată de tranzacție;
- încep să apară deadlock-uri: probabilitatea apariției acestora depinde foarte mult de durata tranzacțiilor, cantitatea de logică de business implicată în tranzacție și de blocaje;
- apare eroarea de expirare a timpului tranzacției;
- serverul "cascadă" din cauza OutOfMemory, dacă sarcina necesită procesarea și modificarea unor volume mari de date, iar existența integrărilor sincrone îngreunează semnificativ fragmentarea procesării în tranzacții mai "ușoare".
Din punct de vedere arhitectural, utilizarea apelurilor blocante în integrare duce la pierderea controlului asupra calității subsystemelor individuale: nu este posibil să se asigure indicatorii de calitate ai unui subsistem în izolarea lor față de indicatorii de calitate ai altui subsistem. Dacă subsistemele sunt dezvoltate de echipe diferite, aceasta este o problemă mare.
Totul devine și mai interesant dacă subsistemele integrate se află în aplicații diferite și este necesară efectuarea de modificări sincronizate din ambele părți. Cum putem asigura tranzacționalitatea acestor modificări?
Dacă modificările sunt efectuate în tranzacții separate, atunci va trebui să se asigure o gestionare fiabilă a excepțiilor și compensațiilor, ceea ce anulează complet avantajul principal al integrărilor sincronizate – simplitatea.
Îmi vin în minte și tranzacțiile distribuite, dar nu le folosim în soluțiile noastre: este dificil să asiguri fiabilitatea.
Saga ca soluție la problema tranzacțiilor
Odată cu creșterea popularității microserviciilor, devine din ce în ce mai solicitat .
Acest tipar rezolvă excelent problemele menționate mai sus legate de tranzacțiile lungi, extinzând în același timp capacitățile de gestionare a stării sistemului din perspectiva logicii de afaceri: compensația după o tranzacție eșuată nu trebuie să restabilească sistemul la starea inițială, ci să asigure o rută alternativă pentru procesarea datelor. Aceasta permite, de asemenea, evitarea repetării pasilor de procesare a datelor care au fost finalizați cu succes în încercările repetate de a aduce procesul la o finalitate "bună".
Interesant este că, în sistemele monolitice, acest tipar este, de asemenea, relevant atunci când vine vorba de integrarea subsistemelor slab legate și se observă efecte negative cauzate de tranzacții lungi și blocări corespunzătoare ale resurselor.
Aplicat proceselor noastre de afaceri în stil BPM, implementarea "Sagi" se dovedește a fi foarte ușoară: pașii individuali ai "Sagi" pot fi definiți ca activități în cadrul procesului de afaceri, iar starea persistentă a procesului de afaceri determină, de asemenea, starea internă a "Sagi". Adică nu avem nevoie de un mecanism de coordonare suplimentar. Va fi necesar doar un broker de mesaje cu suport pentru garanții "at least once" ca transport.
Dar o astfel de soluție are și un "preț":
- logica de afaceri devine mai complexă: este necesar să se gestioneze compensările;
- va fi necesar să renunțăm la full consistency, ceea ce poate fi deosebit de sensibil pentru sistemele monolitice;
- se complică puțin arhitectura, apare o necesitate suplimentară pentru 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 "Saga" nu este atât de evidentă. Pentru microservicii și alte SOA, unde, cel mai probabil, există deja un broker, iar full consistency a fost sacrificată încă de la începutul proiectului, beneficiile utilizării acestui șablon pot depăși semnificativ dezavantajele, în special în prezența unei API convenabile la nivelul logicei 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ă plasăm logica de afaceri a domeniului în raport cu serviciul care asigură persistența datelor domeniului?
Privind arhitectura diferitelor BPMS, poate părea logic să separăm logica de afaceri de persistență: să creăm un strat de microservicii platformă și independente de domeniu, care formează un mediu și un container pentru executarea logicii de afaceri a domeniului, iar persistența datelor domeniului să fie realizată printr-un strat separată din microservicii foarte simple și ușoare. Procesele de afaceri în acest caz orchestruiesc serviciile stratului de persistență.

Această abordare are un avantaj foarte mare: putem adăuga funcționalități platformei nelimitat, iar "îngroșarea" va afecta doar stratul corespunzător al microserviciilor platformei. Procesele de afaceri din orice domeniu primesc imediat posibilitatea de a utiliza noua funcționalitate a platformei, de îndată ce aceasta este actualizată.
O analiză mai detaliată a relevat dezavantaje semnificative ale acestei abordări:
- serviciul platformei care execută logica de afaceri pentru multe domenii prezintă riscuri mari ca punct unic de eșec. Schimbările frecvente în logica de afaceri cresc riscul de erori care pot provoca eșecuri ce se propagă în întregul sistem;
- probleme de performanță: logica de afaceri lucrează cu datele sale printr-o interfață îngustă și lentă:
- datele vor fi procesate din nou și transmise prin stiva de network;
- serviciul de domeniu va returna adesea mai multe date decât sunt necesare pentru logica de afaceri, din cauza capacităților insuficiente de parametrare a interogărilor la nivel de API extern;
- mai multe părți independente ale logicii de afaceri pot solicita din nou aceleași date pentru procesare (această problemă poate fi atenuată prin adăugarea componentelor de sesiune care cache-uiesc datele, dar aceasta complică suplimentar arhitectura și creează probleme de actualitate a datelor și invalidarea cache-ului);
- probleme de tranzacționalitate:
- procesele de afaceri cu stare persistentă, ale căror date sunt gestionate de serviciul platformei, vor deveni incoerente cu datele de domeniu, iar soluții simple pentru această problemă nu sunt prevăzute;
- înlăturarea blocării datelor de domeniu în afara tranzacției: dacă logica de afaceri a domeniului necesită modificări, după efectuarea unei verificări a corectitudinii datelor actuale, trebuie exclusă posibilitatea modificării concurente a datelor procesate. O blocare externă a datelor poate ajuta la rezolvarea problemei, dar o astfel de soluție implică riscuri suplimentare și reduce fiabilitatea generală a sistemului;
- dificultăți suplimentare la actualizare: în anumite cazuri, actualizarea serviciului de persistență și a logicii de afaceri trebuie să fie efectuată sincron sau într-o secvență strictă.
În cele din urmă, a trebuit să ne întoarcem la originile problemei: să încapsulăm datele de domeniu și logica de afaceri a domeniului într-un singur microserviciu. Această abordare simplifică percepția microserviciului ca un component unitar în cadrul sistemului și nu generează problemele menționate mai sus. Acest lucru vine și cu un cost:
- este necesară standardizarea API-ului pentru interacțiunea cu logica de afaceri (în special pentru a sprijini activitățile utilizatorilor în cadrul proceselor de afaceri) și a serviciilor API ale platformei; este nevoie de o atenție mai mare la modificarea API-ului, la compatibilitatea directă și inversă;
- este necesară adăugarea de biblioteci runtime suplimentare pentru a asigura funcționarea logicii de afaceri în cadrul fiecărui microserviciu, ceea ce impune noi cerințe pentru aceste biblioteci: ușurința și un minim de dependențe transitive;
- Dezvoltatorii logicii de afaceri trebuie să urmărească versiunile bibliotecilor: dacă un microserviciu nu a fost actualizat de mult timp, este foarte probabil să conțină o versiune învechită a bibliotecilor. Acest lucru poate deveni un obstacol neașteptat pentru adăugarea unei noi caracteristici și poate necesita migrarea vechii logici de afaceri a acelui serviciu pe versiuni mai noi ale bibliotecilor, dacă între versiuni au fost modificări incompatibile.

Un strat de servicii platforme în astfel de arhitecturi este prezent și el, dar acest strat nu formează un container pentru execuția logicii de afaceri domeniale, ci doar mediul acesteia, oferind funcții „platforme” auxiliare. Un astfel de strat este necesar nu doar pentru a menține ușurința microserviciilor, ci și pentru a centraliza gestionarea.
De exemplu, activitățile utilizatorilor în procesele de afaceri generează sarcini. Cu toate acestea, în lucrul cu sarcinile, utilizatorul trebuie să vadă sarcinile din toate domeniile într-o listă generală, ceea ce înseamnă că trebuie să existe un serviciu platformă corespunzător pentru înregistrarea sarcinilor, curățat de logica de afaceri domenială. Menținerea încapsulării logicii de afaceri în acest context este suficient de problematică, și acesta este un alt compromis al acestei arhitecturi.
Integrarea proceselor de afaceri prin ochii dezvoltatorului aplicațiilor
Așa cum s-a menționat mai sus, dezvoltatorul aplicațiilor trebuie să fie abstractizat de caracteristicile tehnice și inginerești ale implementării interacțiunii între mai multe aplicații, astfel încât să se poată conta pe o bună productivitate a dezvoltării.
Vom încerca să rezolvăm o sarcină de integrare destul de complicată, creată special pentru acest articol. Va fi o sarcină „de joc” implicând trei aplicații, fiecare dintre ele definind un anumit nume de domeniu: „app1”, „app2”, „app3”.
Îndăuntrul fiecărei aplicații se desfășoară procese de afaceri, care încep să „joace mingea” printr-o magistrală de integrare. Mingea va fi reprezentată de mesaje cu numele „Ball”.
Regulile jocului:
- primul jucător – inițiatorul. El invită ceilalți jucători la joc, începe jocul și poate să-l încheie în orice moment;
- ceilalți jucători își anunță participarea la joc, „se fac cunoștință” unii cu alții și cu primul jucător;
- prinzând mingea, jucătorul alege un alt jucător participant și îi transmite mingea. Se ține evidența numărului total de pase;
- Fiecare jucător are o „energie”, care scade cu fiecare pasă a mingii efectuată de acesta. Odată terminată energia, jucătorul se retrage din joc, anunțându-și plecarea;
- dacă jucătorul a rămas singur, el anunță imediat plecarea;
- când toți jucătorii s-au retras, primul jucător anunță finalizarea jocului. Dacă el a părăsit jocul mai devreme, rămâne să supravegheze jocul pentru a-l încheia.
Pentru a rezolva această problemă, voi utiliza DSL-ul nostru pentru procesele de afaceri, care permite descrierea logicii în Kotlin, într-un mod compact, cu minimum de cod repetitiv.
În aplicația app1 va funcționa procesul de afaceri al primului jucător (care este și inițiatorul jocului):
class 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()
// Aceasta este clasa instanței procesului: încorporează starea sa internă
class InitialPlayer : ProcessImpl(initialPlayerModel) {
var playerName: String by persistent("Player1")
var energy: Int by persistent(30)
var players: PlayersList by persistent(PlayersList())
var shotCounter: Int = 0
}
// Aceasta este declarația modelului procesului: creată o dată, utilizată de toate
// instanțele procesului din clasa corespunzătoare
val initialPlayerModel = processModel(name = "InitialPlayer",
version = 1) {
// Conform regulilor, primul jucător este inițiatorul jocului și trebuie să fie unic
uniqueConstraint = UniqueConstraints.singleton
// Declaram activitățile din care este compus procesul de afaceri
val sendNewGameSignal = signal("NewGame")
val sendStopGameSignal = signal("StopGame")
val startTask = humanTask("Start") {
taskOperation {
processCondition { players.size > 0 }
confirmation { "S-au alăturat ${players.size} jucători. Începem?" }
}
}
val stopTask = humanTask("Stop") {
taskOperation {}
}
val waitPlayerJoin = signalWait("PlayerJoin") { signal ->
players.add(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
println("... jucătorul ${signal.data} s-a alăturat ...")
}
val waitPlayerOut = signalWait("PlayerOut") { signal ->
players.remove(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
println("... jucătorul ${signal.data} a ieșit ...")
}
val sendPlayerOut = signal("PlayerOut") {
signalData = { playerName }
}
val sendHandshake = messageSend("Handshake") {
messageData = { playerName }
activation = {
receiverDomain = process.players.last().domain
receiverProcessInstanceId = process.players.last().id
}
}
val throwStartBall = messageSend("Ball") {
messageData = { 1 }
activation = { selectNextPlayer() }
}
val throwBall = messageSend("Ball") {
messageData = { shotCounter + 1 }
activation = { selectNextPlayer() }
onEntry { energy -= 1 }
}
val waitBall = messageWaitData("Ball") {
shotCounter = it
}
// Acum construim graficul procesului din activitățile declarate
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)
}
// Atașăm activităților manipulatoare suplimentare pentru logare
sendNewGameSignal.onExit { println("Să jucăm!") }
sendStopGameSignal.onExit { println("Stop!") }
sendPlayerOut.onExit { println("$playerName: Am ieșit!") }
}
private fun MessageSendInstance.selectNextPlayer() {
val player = process.players.random()
receiverDomain = player.domain
receiverProcessInstanceId = player.id
println("Pasul ${process.shotCounter + 1}: " +
"${process.playerName} >>> ${player.name}")
}Pe lângă implementarea logici de afaceri, codul prezentat poate genera un model obiectual al procesului de afaceri, care poate fi vizualizat sub formă de diagramă. Vizualizatorul nu a fost implementat încă, așa că a trebuit să petrec puțin timp desenând (aici am simplificat puțin notația BPMN în ceea ce privește utilizarea porților pentru a îmbunătăți coerența diagramei cu codul prezentat):

Aplicatia app2 va include procesul de afaceri al unui alt jucător:
class RandomPlayer
import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.instance.MessageSendInstance
data class PlayerInfo(val name: String, val domain: String, val id: String)
class PlayersList: ArrayList()
class RandomPlayer : ProcessImpl(randomPlayerModel) {
var playerName: String by input(persistent = true,
defaultValue = "RandomPlayer")
var energy: Int by input(persistent = true, defaultValue = 30)
var players: PlayersList by persistent(PlayersList())
var allPlayersOut: Boolean by persistent(false)
var shotCounter: Int = 0
val selfPlayer: PlayerInfo
get() = PlayerInfo(playerName, env.eventDispatcher.domainName, id)
}
val randomPlayerModel = processModel(name = "RandomPlayer",
version = 1) {
val waitNewGameSignal = signalWait("NewGame")
val waitStopGameSignal = signalWait("StopGame")
val sendPlayerJoin = signal("PlayerJoin") {
signalData = { playerName }
}
val sendPlayerOut = signal("PlayerOut") {
signalData = { playerName }
}
val waitPlayerJoin = signalWaitCustom("PlayerJoin") {
eventCondition = { signal ->
signal.sender.processInstanceId != process.id
&& !process.players.any { signal.sender.processInstanceId == it.id}
}
handler = { signal ->
players.add(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
}
}
val waitPlayerOut = signalWait("PlayerOut") { signal ->
players.remove(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
allPlayersOut = players.isEmpty()
}
val sendHandshake = messageSend("Handshake") {
messageData = { playerName }
activation = {
receiverDomain = process.players.last().domain
receiverProcessInstanceId = process.players.last().id
}
}
val receiveHandshake = messageWait("Handshake") { message ->
if (!players.any { message.sender.processInstanceId == it.id}) {
players.add(PlayerInfo(
message.data!!,
message.sender.domain,
message.sender.processInstanceId))
}
}
val throwBall = messageSend("Ball") {
messageData = { shotCounter + 1 }
activation = { selectNextPlayer() }
onEntry { energy -= 1 }
}
val waitBall = messageWaitData("Ball") {
shotCounter = it
}
startFrom(waitNewGameSignal)
.fork("mainFork") {
next(sendPlayerJoin)
.branch("mainLoop") {
ifTrue { energy < 5 || allPlayersOut }
.next(sendPlayerOut)
.next(waitBall)
ifElse()
.next(waitBall)
.next(throwBall)
.loop()
}
next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
next(waitPlayerOut).next(waitPlayerOut)
next(receiveHandshake).next(receiveHandshake)
next(waitStopGameSignal).terminate()
}
sendPlayerJoin.onExit { println("$playerName: I'm here!") }
sendPlayerOut.onExit { println("$playerName: I'm out!") }
}
private fun MessageSendInstance.selectNextPlayer() {
val player = if (process.players.isNotEmpty())
process.players.random()
else
process.selfPlayer
receiverDomain = player.domain
receiverProcessInstanceId = player.id
println("Step ${process.shotCounter + 1}: " +
"${process.playerName} >>> ${player.name}")
}Diagrama:

În aplicația app3, vom face ca jucătorul să aibă un comportament ușor diferit: în loc de a alege aleator următorul jucător, acesta va acționa conform algoritmului round-robin:
class RoundRobinPlayer
import ru.krista.bpm.ProcessInstance
import ru.krista.bpm.runtime.ProcessImpl
import ru.krista.bpm.runtime.dsl.processModel
import ru.krista.bpm.runtime.instance.MessageSendInstance
data class PlayerInfo(val name: String, val domain: String, val id: String)
class PlayersList: ArrayList()
class RoundRobinPlayer : ProcessImpl(roundRobinPlayerModel) {
var playerName: String by input(persistent = true,
defaultValue = "RoundRobinPlayer")
var energy: Int by input(persistent = true, defaultValue = 30)
var players: PlayersList by persistent(PlayersList())
var nextPlayerIndex: Int by persistent(-1)
var allPlayersOut: Boolean by persistent(false)
var shotCounter: Int = 0
val selfPlayer: PlayerInfo
get() = PlayerInfo(playerName, env.eventDispatcher.domainName, id)
}
val roundRobinPlayerModel = processModel(
name = "RoundRobinPlayer",
version = 1) {
val waitNewGameSignal = signalWait("NewGame")
val waitStopGameSignal = signalWait("StopGame")
val sendPlayerJoin = signal("PlayerJoin") {
signalData = { playerName }
}
val sendPlayerOut = signal("PlayerOut") {
signalData = { playerName }
}
val waitPlayerJoin = signalWaitCustom("PlayerJoin") {
eventCondition = { signal ->
signal.sender.processInstanceId != process.id
&& !process.players.any { signal.sender.processInstanceId == it.id}
}
handler = { signal ->
players.add(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
}
}
val waitPlayerOut = signalWait("PlayerOut") { signal ->
players.remove(PlayerInfo(
signal.data!!,
signal.sender.domain,
signal.sender.processInstanceId))
allPlayersOut = players.isEmpty()
}
val sendHandshake = messageSend("Handshake") {
messageData = { playerName }
activation = {
receiverDomain = process.players.last().domain
receiverProcessInstanceId = process.players.last().id
}
}
val receiveHandshake = messageWait("Handshake") { message ->
if (!players.any { message.sender.processInstanceId == it.id}) {
players.add(PlayerInfo(
message.data!!,
message.sender.domain,
message.sender.processInstanceId))
}
}
val throwBall = messageSend("Ball") {
messageData = { shotCounter + 1 }
activation = { selectNextPlayer() }
onEntry { energy -= 1 }
}
val waitBall = messageWaitData("Ball") {
shotCounter = it
}
startFrom(waitNewGameSignal)
.fork("mainFork") {
next(sendPlayerJoin)
.branch("mainLoop") {
ifTrue { energy < 5 || allPlayersOut }
.next(sendPlayerOut)
.next(waitBall)
ifElse()
.next(waitBall)
.next(throwBall)
.loop()
}
next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
next(waitPlayerOut).next(waitPlayerOut)
next(receiveHandshake).next(receiveHandshake)
next(waitStopGameSignal).terminate()
}
sendPlayerJoin.onExit { println("$playerName: I'm here!") }
sendPlayerOut.onExit { println("$playerName: I'm out!") }
}
private fun MessageSendInstance.selectNextPlayer() {
var idx = process.nextPlayerIndex + 1
if (idx >= process.players.size) {
idx = 0
}
process.nextPlayerIndex = idx
val player = if (process.players.isNotEmpty())
process.players[idx]
else
process.selfPlayer
receiverDomain = player.domain
receiverProcessInstanceId = player.id
println("Step ${process.shotCounter + 1}: " +
"${process.playerName} >>> ${player.name}")
}În rest, comportamentul jucătorului nu diferă de cel anterior, așa că diagrama nu se schimbă.
Acum avem nevoie de un test pentru a rula tot acest cod. Voi prezenta doar codul testului, pentru a nu aglomera articolul cu boilerplate (de fapt, am folosit mediul de testare creat anterior pentru testarea integrării 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");
// Acum trebuie să așteptăm puțin, ca jucătorii să se "cunoască" între ei.
// Așteptarea prin sleep este o soluție proastă, dar cea mai simplă.
// Nu faceți asta în teste serioase!
Thread.sleep(1000);
// Începem jocul, închizând activitatea utilizatorului
assertTrue(closeTask(app1, pl1, "Start"));
app1.getWaiting().waitProcessFinished(pl1);
app2.getWaiting().waitProcessFinished(pl2);
app2.getWaiting().waitProcessFinished(pl3);
app3.getWaiting().waitProcessFinished(pl4);
app3.getWaiting().waitProcessFinished(pl5);
}
private Map playerParams(String name, int energy) {
Map params = new HashMap();
params.put("playerName", name);
params.put("energy", energy);
return params;
}Pornim testul, analizăm jurnalul:
ieșire consolă
S-a preluat blocarea cheii lock://app1/process/InitialPlayer
Haideți să jucăm!
S-a ridicat blocarea cheii lock://app1/process/InitialPlayer
Player2: Sunt aici!
Player3: Sunt aici!
Player4: Sunt aici!
Player5: Sunt aici!
... alătură-te jucătorului Player2 ...
... alătură-te jucătorului Player4 ...
... alătură-te jucătorului Player3 ...
... alătură-te jucătorului Player5 ...
Pasul 1: Player1 >>> Player3
Pasul 2: Player3 >>> Player5
Pasul 3: Player5 >>> Player3
Pasul 4: Player3 >>> Player4
Pasul 5: Player4 >>> Player3
Pasul 6: Player3 >>> Player4
Pasul 7: Player4 >>> Player5
Pasul 8: Player5 >>> Player2
Pasul 9: Player2 >>> Player5
Pasul 10: Player5 >>> Player4
Pasul 11: Player4 >>> Player2
Pasul 12: Player2 >>> Player4
Pasul 13: Player4 >>> Player1
Pasul 14: Player1 >>> Player4
Pasul 15: Player4 >>> Player3
Pasul 16: Player3 >>> Player1
Pasul 17: Player1 >>> Player2
Pasul 18: Player2 >>> Player3
Pasul 19: Player3 >>> Player1
Pasul 20: Player1 >>> Player5
Pasul 21: Player5 >>> Player1
Pasul 22: Player1 >>> Player2
Pasul 23: Player2 >>> Player4
Pasul 24: Player4 >>> Player5
Pasul 25: Player5 >>> Player3
Pasul 26: Player3 >>> Player4
Pasul 27: Player4 >>> Player2
Pasul 28: Player2 >>> Player5
Pasul 29: Player5 >>> Player2
Pasul 30: Player2 >>> Player1
Pasul 31: Player1 >>> Player3
Pasul 32: Player3 >>> Player4
Pasul 33: Player4 >>> Player1
Pasul 34: Player1 >>> Player3
Pasul 35: Player3 >>> Player4
Pasul 36: Player4 >>> Player3
Pasul 37: Player3 >>> Player2
Pasul 38: Player2 >>> Player5
Pasul 39: Player5 >>> Player4
Pasul 40: Player4 >>> Player5
Pasul 41: Player5 >>> Player1
Pasul 42: Player1 >>> Player5
Pasul 43: Player5 >>> Player3
Pasul 44: Player3 >>> Player5
Pasul 45: Player5 >>> Player2
Pasul 46: Player2 >>> Player3
Pasul 47: Player3 >>> Player2
Pasul 48: Player2 >>> Player5
Pasul 49: Player5 >>> Player4
Pasul 50: Player4 >>> Player2
Pasul 51: Player2 >>> Player5
Pasul 52: Player5 >>> Player1
Pasul 53: Player1 >>> Player5
Pasul 54: Player5 >>> Player3
Pasul 55: Player3 >>> Player5
Pasul 56: Player5 >>> Player2
Pasul 57: Player2 >>> Player1
Pasul 58: Player1 >>> Player4
Pasul 59: Player4 >>> Player1
Pasul 60: Player1 >>> Player4
Pasul 61: Player4 >>> Player3
Pasul 62: Player3 >>> Player2
Pasul 63: Player2 >>> Player5
Pasul 64: Player5 >>> Player4
Pasul 65: Player4 >>> Player5
Pasul 66: Player5 >>> Player1
Pasul 67: Player1 >>> Player5
Pasul 68: Player5 >>> Player3
Pasul 69: Player3 >>> Player4
Pasul 70: Player4 >>> Player2
Pasul 71: Player2 >>> Player5
Pasul 72: Player5 >>> Player2
Pasul 73: Player2 >>> Player1
Pasul 74: Player1 >>> Player4
Pasul 75: Player4 >>> Player1
Pasul 76: Player1 >>> Player2
Pasul 77: Player2 >>> Player5
Pasul 78: Player5 >>> Player4
Pasul 79: Player4 >>> Player3
Pasul 80: Player3 >>> Player1
Pasul 81: Player1 >>> Player5
Pasul 82: Player5 >>> Player1
Pasul 83: Player1 >>> Player4
Pasul 84: Player4 >>> Player5
Pasul 85: Player5 >>> Player3
Pasul 86: Player3 >>> Player5
Pasul 87: Player5 >>> Player2
Pasul 88: Player2 >>> Player3
Player2: Sunt plecat!
Pasul 89: Player3 >>> Player4
... jucătorul Player2 a ieșit ...
Pasul 90: Player4 >>> Player1
Pasul 91: Player1 >>> Player3
Pasul 92: Player3 >>> Player1
Pasul 93: Player1 >>> Player4
Pasul 94: Player4 >>> Player3
Pasul 95: Player3 >>> Player5
Pasul 96: Player5 >>> Player1
Pasul 97: Player1 >>> Player5
Pasul 98: Player5 >>> Player3
Pasul 99: Player3 >>> Player5
Pasul 100: Player5 >>> Player4
Pasul 101: Player4 >>> Player5
Player4: Sunt plecat!
... jucătorul Player4 a ieșit ...
Pasul 102: Player5 >>> Player1
Pasul 103: Player1 >>> Player3
Pasul 104: Player3 >>> Player1
Pasul 105: Player1 >>> Player3
Pasul 106: Player3 >>> Player5
Pasul 107: Player5 >>> Player3
Pasul 108: Player3 >>> Player1
Pasul 109: Player1 >>> Player3
Pasul 110: Player3 >>> Player5
Pasul 111: Player5 >>> Player1
Pasul 112: Player1 >>> Player3
Pasul 113: Player3 >>> Player5
Pasul 114: Player5 >>> Player3
Pasul 115: Player3 >>> Player1
Pasul 116: Player1 >>> Player3
Pasul 117: Player3 >>> Player5
Pasul 118: Player5 >>> Player1
Pasul 119: Player1 >>> Player3
Pasul 120: Player3 >>> Player5
Pasul 121: Player5 >>> Player3
Player5: Sunt plecat!
... jucătorul Player5 a ieșit ...
Pasul 122: Player3 >>> Player5
Pasul 123: Player5 >>> Player1
Player5: Sunt plecat!
Pasul 124: Player1 >>> Player3
... jucătorul Player5 a ieșit ...
Pasul 125: Player3 >>> Player1
Pasul 126: Player1 >>> Player3
Player1: Sunt plecat!
... jucătorul Player1 a ieșit ...
Pasul 127: Player3 >>> Player3
Player3: Sunt plecat!
Stop!
Pasul 128: Player3 >>> Player3
Player3: Sunt plecat!Din toate acestea, se pot trasa câteva concluzii importante:
- dacă există uneltele necesare, dezvoltatorii aplicați pot crea interacțiuni de integrare între aplicații fără a se abate de la logica de afaceri;
- complexitatea (complexity) sarcinii de integrare, ce necesită competențe inginerie, poate fi ascunsă în cadrul unui framework, dacă aceasta este încorporată de la bun început în arhitectura framework-ului. Dificultatea sarcinii (difficulty) nu poate fi ascunsă, așadar soluția unei sarcini dificile în cod va arăta corespunzător;
- când se dezvoltă logica de integrare, este absolut necesar să se ia în considerare consistența eventuală și absența liniarizabilității modificării stării tuturor participanților la integrare. Aceasta obligă la complexificarea logicii, pentru a o face insensibilă la ordinea apariției evenimentelor externe. În exemplul nostru, un jucător este obligat să participe la joc doar după ce a anunțat ieșirea sa din joc: ceilalți jucători vor continua să-i paseze mingea, până când informația despre ieșirea sa va ajunge și va fi procesată de toți participanții. Această logică nu decurge din regulile jocului și reprezintă o soluție compromis în cadrul arhitecturii alese.
Apoi, vom discuta despre diferitele subtilități ale soluției noastre, compromisurile și alte aspecte.
Toate mesajele – într-o singură coadă
Toate aplicațiile ce fac integrare lucrează cu o singură bus de integrare, reprezentată sub forma unui broker extern, o coadă BPMQueue – pentru mesaje și un topic BPMTopic – pentru semnale (evenimente). Trecerea tuturor mesajelor printr-o singură coadă este în sine un compromis. La nivelul logicii de afaceri se pot introduce acum nenumărate tipuri noi de mesaje, fără a aduce modificări structurii sistemului. Aceasta reprezintă o simplificare semnificativă, dar implică anumite riscuri, care, în contextul sarcinilor noastre tipice, ne-au părut nesemnificative.

Totuși, există un detaliu: fiecare aplicație filtrează mesajele "proprii" din coadă încă de la intrare, după numele domeniului său. De asemenea, domeniul poate fi specificat și în semnale, dacă este necesar să se restricționeze "domeniul de vizibilitate" al semnalului la o singură aplicație. Acest lucru ar trebui să crească lățimea de bandă a bus-ului, dar logica de afaceri trebuie acum să opereze cu nume de domenii: obligatoriu pentru adresarea mesajelor și preferabil pentru semnale.
Asigurarea fiabilității bus-ului de integrare
Fiabilitatea se bazează pe mai multe aspecte:
- brokerul de mesaje ales – un component critic al arhitecturii și un punct unic de defectare: acesta trebuie să fie suficient de rezistent la defecte. Este recomandat să se folosească doar implementări dovedite în timp, cu suport bun și o comunitate mare;
- este necesară asigurarea unei disponibilități ridicate a brokerului de mesaje, ceea ce înseamnă că acesta trebuie să fie fizic separat de aplicațiile integrate (asigurarea disponibilității ridicate a aplicațiilor cu logică de afaceri este semnificativ mai complicată și mai costisitoare);
- brokerul trebuie să asigure garanții de livrare "at least once". Aceasta este o cerință obligatorie pentru funcționarea fiabilă a bus-ului de integrare. Nu este necesară garanția la nivelul "exactly once": procesele de afaceri, de obicei, nu sunt sensibile la livrarea repetată a mesajelor sau evenimentelor, iar în anumite sarcini unde acest lucru este important, este mai simplu să se adauge o verificare suplimentară în logica de afaceri decât să se folosească constant garanții "destul de costisitoare";
- trimiterea mesajelor și semnalelor trebuie să fie implicată într-o tranzacție comună cu modificarea stării proceselor de afaceri și a datelor domeniului. Opțiunea preferată ar fi utilizarea modelului , dar aceasta va necesita o tabelă suplimentară în baza de date și un retransmițător. În aplicațiile JEE, acest aspect poate fi simplificat prin utilizarea unui manager JTA local, dar conexiunea la brokerul ales trebuie să fie capabilă să funcționeze în modul ;
- handlerii mesajelor și evenimentelor de intrare trebuie să funcționeze de asemenea cu tranzacția de modificare a stării procesului de afaceri: dacă o astfel de tranzacție este anullată, atunci și primirea mesajului trebuie să fie anulată;
- mesajele care nu au putut fi livrate din cauza erorilor trebuie stocate într-un depozit separat (Dead Letter Queue). Am creat un microserviciu platformă separat care salvează aceste mesaje în stocarea sa, le indexează după atribute (pentru grupare și căutare rapidă) și oferă o API pentru vizualizarea, retrimiterea către destinație și ștergerea mesajelor. Administratorii sistemului pot interacționa cu acest serviciu prin intermediul interfeței sale web;
- în setările brokerului trebuie ajustat numărul de încercări de livrare și întârzierile dintre livrări pentru a reduce probabilitatea ca mesajele să ajungă în DLQ (calcularea parametrilor optimi este practic imposibilă, dar se pot face ajustări empiric pe parcursul utilizării);
- stocarea DLQ trebuie monitorizată continuu, iar sistemul de monitorizare trebuie să alerteze administratorii sistemului, astfel încât, în cazul apariției mesajelor nedeliverate, să se reacționeze cât mai repede posibil. Aceasta va reduce "zona de afectare" a unei defecțiuni sau erori în logica de afaceri;
- bus-ul de integrare trebuie să fie insensibil la absența temporară a aplicațiilor: abonamentele la topic trebuie să fie durabile, iar numele de domeniu al aplicației trebuie să fie unic, astfel încât, în timpul absenței aplicației, mesajele sale din coadă să nu fie preluate de altcineva.
Asigurarea securității firelor procesului de afaceri
Același exemplar al procesului de afaceri poate primi simultan mai multe mesaje și evenimente, ale căror procesare va fi inițiată în paralel. Între timp, pentru dezvoltatorul aplicației totul trebuie să fie simplu și sigur din perspectiva firelor.
Logica de afaceri a procesului prelucrează fiecare eveniment extern care afectează acest proces de afaceri, separat. Aceste evenimente pot fi:
- lansarea unui exemplar al procesului de afaceri;
- acțiunea utilizatorului referitoare la activitatea din cadrul procesului de afaceri;
- primirea unui mesaj sau semnal la care este abonat exemplar procesului de afaceri;
- activarea unui cronometru stabilit de exemplar procesului de afaceri;
- intervenția prin API (de exemplu, întreruperea de urgență a procesului).
Fiecare astfel de eveniment poate schimba starea instanței procesului de afaceri: unele activități pot fi finalizate și pot începe altele, iar valorile proprietăților persistente pot suferi modificări. Închiderea oricărei activități poate duce la activarea uneia sau mai multor activități următoare. Acestea, la rândul lor, pot aștepta alte evenimente sau, dacă nu necesită date suplimentare, pot fi finalizate în aceeași tranzacție. Înainte de a închide tranzacția, noua stare a procesului de afaceri este salvată în baza de date, unde va aștepta următorul eveniment extern.
Datele persistente ale procesului de afaceri, salvate în baza de date relațională, reprezintă un punct de sincronizare foarte convenabil al procesării, dacă se folosește SELECT FOR UPDATE. Dacă unei tranzacții îi reușește să obțină starea procesului de afaceri din bază pentru a o modifica, atunci nicio altă tranzacție nu va putea obține simultan aceeași stare pentru o altă modificare, iar după finalizarea primei tranzacții, a doua va primi garantat deja starea modificată.
Folosind blocări pesimiste pe partea SGBD-ului, îndeplinim toate cerințele necesare , și păstrăm și posibilitatea de scalabilitate a aplicației cu logică de afaceri prin creșterea numărului de instanțe active.
Cu toate acestea, blocările pesimiste ne amenință cu deadlock-uri, așadar, SELECT FOR UPDATE ar trebui să fie totuși limitat la un timeout rezonabil în caz de apariție a deadlock-urilor în diverse cazuri critice în logica de afaceri.
O altă problemă este sincronizarea începutului procesului de afaceri. Atâta timp cât nu există o instanță a procesului de afaceri, nu există nici starea sa în bază, prin urmare metoda descrisă nu este aplicabilă. Dacă trebuie să asigurăm unicitatea instanței procesului de afaceri într-un anumit domeniu, atunci va fi necesar un obiect de sincronizare, asociat cu clasa procesului și domeniul corespunzător. Pentru a rezolva această problemă, folosim un alt mecanism de blocare, care permite obținerea unei blocări a unei resurse arbitrare, specificată printr-o cheie în format URI, printr-un serviciu extern.
În exemplele noastre, procesul de afaceri InitialPlayer conține o declarație
uniqueConstraint = UniqueConstraints.singletonPrin urmare, jurnalul conține mesaje despre obținerea și eliberarea blocării corespunzătoare a cheii. Nu există astfel de mesaje pentru alte procese de afaceri: uniqueConstraint nu este definit.
Problemele proceselor de afaceri cu starea persistentă
Uneori, prezența stării persistente nu doar că ajută, dar poate și complica foarte mult dezvoltarea.
Problemele apar atunci când este necesar să se facă modificări în logica de afaceri și/sau modelul procesului de afaceri. Nu orice astfel de modificare se dovedește a fi compatibilă cu starea veche a proceselor de afaceri. Dacă în baza de date există multe instanțe „active”, atunci realizarea de modificări incompatibile poate cauza multe neplăceri, cu care ne-am confruntat adesea în utilizarea jBPM.
În funcție de adâncimea modificărilor, se pot urma două căi:
- crearea unui nou tip de proces de afaceri pentru a nu face modificări incompatibile în vechiul proces și utilizarea acestuia în locul vechiului la inițierea de noi instanțe. Instanțele vechi vor continua să funcționeze „în stilul vechi”;
- migrarea stării persistente a proceselor de afaceri în timpul actualizării logicii de afaceri.
Prima cale este mai simplă, dar are propriile restricții și dezavantaje, de exemplu:
- duplicarea logicii de afaceri în multe modele de procese de afaceri, creșterea volumului logicii de afaceri;
- adesea este necesar un tranziție instantanee la noua logică de afaceri (în ceea ce privește sarcinile de integrare - aproape întotdeauna);
- dezvoltatorul nu știe când este momentul potrivit pentru a șterge modelele î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 păstrată într-un format ușor de citit și de procesat: într-o linie de format JSON. Aceasta permite migrarea atât în interiorul aplicației, cât și în afara acesteia. În caz extrem, se poate ajusta manual (în special util în dezvoltare în timpul depanării);
- logica de afaceri de integrare nu folosește nume ale proceselor de afaceri, astfel încât în orice moment să se poată înlocui implementarea unuia dintre procesele implicate cu una nouă, cu un nou nume (de exemplu, „InitialPlayerV2”). Legătura se realizează prin numele mesajelor și semnalelor;
- modelul de proces are un număr de versiune pe care îl creștem atunci când facem modificări incompatibile în această modelă, iar acest număr este păstrat împreună cu starea instanței procesului;
- starea persistentă a procesului este citită din baza de date mai întâi într-un model de obiecte convenabil, cu care poate lucra procedura de migrare, dacă numărul versiunii modelului s-a schimbat;
- procedura de migrare este plasată lângă logica de afaceri și este apelată "leneș" pentru fiecare instanță a procesului de afaceri în momentul restaurării sale din baza de date;
- dacă este necesar să migrăm starea tuturor instanțelor procesului rapid și sincron, se aplică soluții mai clasice de migrare a DB, dar acolo trebuie să lucrăm cu JSON.
Este nevoie de un alt cadru pentru procesele de afaceri?
Soluțiile descrise în articol ne-au permis să simplificăm semnificativ viața, să extindem gama de probleme rezolvate la nivelul dezvoltării aplicațiilor, să facem ideile de extragere a logicii de afaceri în microservicii mai atractive. Pentru aceasta, s-au depus multe eforturi, s-a creat un cadru foarte „ușor” pentru procesele de afaceri, precum și componente auxiliare pentru a rezolva problemele menționate în contextul unei game largi de sarcini aplicaționale. Avem dorința de a împărtăși aceste rezultate, de a face dezvoltarea componentelor comune accesibilă publicului sub o licență liberă. Acest lucru va necesita eforturi și timp considerabile. Înțelegerea cererii pentru astfel de soluții ar putea fi un stimulent suplimentar pentru noi. În articolul propus, s-a acordat foarte puțină atenție capacităților cadrelor în sine, dar unele dintre ele sunt vizibile din exemplele prezentate. Dacă totuși vom publica cadrul nostru, acesta va fi dedicat unui articol separat. Până atunci, am aprecia dacă ați lăsa un mic feedback, răspunzând la întrebarea:
Numai utilizatorii înregistrați pot participa la sondaj. , vă rugăm.
Este nevoie de un alt cadru pentru procesele de afaceri?
18,8%da, căutăm de ceva timp ceva similar3
12,5%mi-ar plăcea să aflu mai multe despre implementarea voastră, s-ar putea să-mi fie util2
6,2%folosim unul din cadrele existente, dar ne gândim să-l înlocuim1
18,8%folosim unul din cadrele existente, suntem mulțumiți3
18,8%ne descurcăm fără cadru3
25,0%scriem propriul4
16 utilizatori au votat. 7 utilizatori s-au abținut.
Sursa: habr.com
