Nia firmao specialiĝas pri la disvolviĝo de ERP-klasaj programaj solvoj, kies plej granda parto estas okupata de transakciaj sistemoj kun grandega kvanto de komerca logiko kaj dokumentfluo laŭ EDMS. Nunaj versioj de niaj produktoj baziĝas sur JavaEE-teknologioj, sed ni ankaŭ aktive eksperimentas kun mikroservoj. Unu el la plej problemaj areoj de tiaj solvoj estas la integriĝo de diversaj subsistemoj apartenantaj al apudaj domajnoj. Integrigaj problemoj ĉiam donis al ni grandegan kapdoloron, sendepende de la arkitekturaj stiloj, teknologiaj stakoj kaj kadroj, kiujn ni uzas, sed lastatempe estis progreso en solvado de tiaj problemoj.
En la artikolo, kiun mi atentigas, mi parolos pri la sperto kaj arkitektura esplorado, kiun NPO Krista havas en la difinita areo. Ni ankaŭ rigardos ekzemplon de simpla solvo al integriga problemo el la vidpunkto de programisto de aplikaĵo kaj ekscios, kio kaŝiĝas malantaŭ ĉi tiu simpleco.
Malgarantio
La arkitekturaj kaj teknikaj solvoj priskribitaj en la artikolo estas proponitaj de mi surbaze de persona sperto en la kunteksto de specifaj taskoj. Ĉi tiuj solvoj ne pretendas esti universalaj kaj eble ne estas optimumaj sub aliaj uzkondiĉoj.
Kion rilatas BPM al ĝi?
Por respondi ĉi tiun demandon, ni devas iom pli profundiĝi en la specifaĵoj de la aplikataj problemoj de niaj solvoj. La ĉefa parto de la komerca logiko en nia tipa transakcia sistemo estas enigi datumojn en la datumbazon per uzantinterfacoj, mana kaj aŭtomatigita konfirmo de ĉi tiuj datumoj, efektivigi ĝin per iu laborfluo, publikigi ĝin al alia sistemo / analiza datumbazo / arkivo, generi raportojn. . Tiel, la ŝlosila funkcio de la sistemo por klientoj estas la aŭtomatigo de iliaj internaj komercaj procezoj.
Por oportuno, ni uzas la terminon "dokumento" en komunikado kiel ia abstraktado de aro de datumoj kunigitaj per komuna ŝlosilo al kiu certa laborfluo povas esti "ligita".
Sed kio pri integriga logiko? Post ĉio, la integriga tasko estas generita de la arkitekturo de la sistemo, kiu estas "tranĉita" en partojn NE laŭ la peto de la kliento, sed sub la influo de tute malsamaj faktoroj:
submetita al la leĝo de Conway;
kiel rezulto de reuzo de subsistemoj antaŭe evoluigitaj por aliaj produktoj;
laŭ la bontrovo de la arkitekto, surbaze de nefunkciaj postuloj.
Estas granda tento apartigi integrigan logikon de la komerca logiko de la ĉefa laborfluo, por ne malpurigi la komercan logikon per integrigaj artefaktoj kaj savi la programiston de aplikaĵo de la bezono enprofundiĝi en la trajtojn de la arkitektura pejzaĝo de la sistemo. Ĉi tiu aliro havas kelkajn avantaĝojn, sed praktiko montras ĝian neefikecon:
solvi integrigajn problemojn kutime falas reen al la plej simplaj opcioj en la formo de sinkronaj vokoj pro la limigitaj etendpunktoj en la efektivigo de la ĉefa laborfluo (la malavantaĝoj de sinkrona integriĝo estas diskutitaj malsupre);
integriĝartefaktoj daŭre penetras kernan komerclogikon kiam religo de alia subsistemo estas postulata;
la programisto ignoras la integriĝon kaj povas facile rompi ĝin ŝanĝante la laborfluon;
la sistemo ĉesas esti ununura tuto de la vidpunkto de la uzanto, "kudroj" inter subsistemoj iĝas videblaj, kaj redundaj uzantoperacioj aperas, iniciatante la translokigon de datenoj de unu subsistemo al alia.
Alia aliro estas konsideri integrigajn interagojn kiel integran parton de la kerna komerca logiko kaj laborfluo. Por malhelpi kvalifikojn de aplikaĵaj programistoj altiĝo, krei novajn integrigajn interagojn devus esti facila kaj senpene, kun minimumaj elektoj por elekti solvon. Ĉi tio estas pli malfacila ol ĝi ŝajnas: la ilo devas esti sufiĉe potenca por provizi la uzanton per la bezonataj elektoj por ĝia uzo, sen permesi al li "pafi sin en la piedon". Estas multaj demandoj, kiujn inĝeniero devas respondi en la kunteksto de integrigaj taskoj, sed pri kiuj aplikaĵprogramisto ne devus pensi en sia ĉiutaga laboro: transakciaj limoj, konsistenco, atomeco, sekureco, skalo, ŝarĝo kaj rimedo-distribuo, enrutado, marŝalado, kuntekstoj de distribuo kaj ŝanĝado ktp. Necesas proponi al programistoj de aplikaĵoj sufiĉe simplaj solvŝablonoj, en kiuj la respondoj al ĉiuj tiaj demandoj estas jam kaŝitaj. Ĉi tiuj ŝablonoj devas esti sufiĉe sekuraj: komerca logiko ŝanĝiĝas tre ofte, kio pliigas la riskon de enkonduko de eraroj, la kosto de eraroj devas resti sur sufiĉe malalta nivelo.
Sed kion rilatas BPM al ĝi? Estas multaj ebloj por efektivigi laborfluon...
Efektive, alia efektivigo de komercaj procezoj estas tre populara en niaj solvoj - per la deklara difino de ŝtata transira diagramo kaj la ligo de pritraktantoj kun komerca logiko por transiroj. En ĉi tiu kazo, la stato, kiu determinas la nunan pozicion de la "dokumento" en la komerca procezo, estas atributo de la "dokumento" mem.
Jen kiel aspektas la procezo ĉe la komenco de projekto
La populareco de ĉi tiu efektivigo ŝuldiĝas al la relativa simpleco kaj rapideco de kreado de linearaj komercaj procezoj. Tamen, ĉar softvarsistemoj daŭre iĝas pli kompleksaj, la aŭtomatigita parto de la komerca procezo kreskas kaj iĝas pli kompleksa. Estas bezono de putriĝo, reuzo de partoj de procezoj, same kiel disbranĉaj procezoj tiel ke ĉiu branĉo estas ekzekutita paralele. Sub tiaj kondiĉoj, la ilo fariĝas maloportuna, kaj la ŝtattransira diagramo perdas sian informenhavon (integrigaj interagoj tute ne reflektiĝas en la diagramo).
Jen kiel aspektas la procezo post pluraj ripetoj de klarigo de postuloj.
La elirejo de ĉi tiu situacio estis la integriĝo de la motoro jBPM en iujn produktojn kun la plej kompleksaj komercaj procezoj. Baldaŭ, ĉi tiu solvo havis iom da sukceso: fariĝis eble efektivigi kompleksajn komercajn procezojn konservante sufiĉe informan kaj koncernan diagramon en la notacio. BPMN2.
Malgranda parto de kompleksa komerca procezo
Longtempe, la solvo ne plenumis atendojn: la alta laborintenso de kreado de komercaj procezoj per vidaj iloj ne permesis atingi akcepteblajn produktivajn indikilojn, kaj la ilo mem fariĝis unu el la plej malŝatataj inter programistoj. Ankaŭ estis plendoj pri la interna strukturo de la motoro, kio kaŭzis la aperon de multaj "pecetoj" kaj "lambastonoj".
La ĉefa pozitiva aspekto de uzado de jBPM estis la konscio pri la avantaĝoj kaj damaĝoj de havi la propran konstantan staton de komerca procezo. Ni ankaŭ vidis la eblecon uzi procezan aliron por efektivigi kompleksajn integrigajn protokolojn inter malsamaj aplikoj uzante nesinkronajn interagojn per signaloj kaj mesaĝoj. La ĉeesto de persista ŝtato ludas decidan rolon en tio.
Surbaze de ĉi-supra, ni povas konkludi: La proceza aliro en la BPM-stilo permesas al ni solvi larĝan gamon de taskoj por aŭtomatigi ĉiam pli kompleksajn komercajn procezojn, harmonie adapti integrigajn agadojn en ĉi tiujn procezojn kaj konservi la kapablon videble montri la efektivigitan procezon en taŭga notacio.
Malavantaĝoj de sinkronaj vokoj kiel integriga ŝablono
Sinkrona integriĝo rilatas al la plej simpla bloka voko. Unu subsistemo funkcias kiel la servila flanko kaj elmontras la API kun la postulata metodo. Alia subsistemo funkcias kiel la klienta flanko kaj en la ĝusta tempo faras vokon kaj atendas la rezulton. Depende de la sistema arkitekturo, la kliento kaj servilflankoj povas esti lokitaj aŭ en la sama aplikaĵo kaj procezo, aŭ en malsamaj. En la dua kazo, vi devas apliki iun RPC-efektivigon kaj provizi marŝadon de la parametroj kaj la rezulto de la voko.
Ĉi tiu integriga ŝablono havas sufiĉe grandan aron da malavantaĝoj, sed ĝi estas tre vaste uzata en la praktiko pro sia simpleco. La rapideco de efektivigo allogas kaj devigas vin uzi ĝin denove kaj denove antaŭ premaj limdatoj, registrante la solvon kiel teknika ŝuldo. Sed ankaŭ okazas, ke nespertaj programistoj uzas ĝin senkonscie, simple ne konsciante la negativajn konsekvencojn.
Krom la plej evidenta pliiĝo en subsistema konektebleco, ekzistas ankaŭ malpli evidentaj problemoj kun "kreskado" kaj "streĉado" transakcioj. Efektive, se la komerca logiko faras iujn ŝanĝojn, tiam transakcioj ne povas esti evititaj, kaj transakcioj, siavice, blokas certajn aplikajn rimedojn trafitaj de ĉi tiuj ŝanĝoj. Tio estas, ĝis unu subsistemo atendas respondon de la alia, ĝi ne povos kompletigi la transakcion kaj forigi la serurojn. Ĉi tio signife pliigas la riskon de diversaj efikoj:
La respondeco de la sistemo perdiĝas, uzantoj longe atendas respondojn al petoj;
la servilo ĝenerale ĉesas respondi al uzantpetoj pro troplena fadena naĝejo: la plimulto de fadenoj estas ŝlositaj sur rimedo okupita de transakcio;
Senblokoj komencas aperi: la verŝajneco de ilia okazo forte dependas de la daŭro de transakcioj, la kvanto de komerca logiko kaj seruroj implikitaj en la transakcio;
ekaperas eraroj pri transakcia tempo-tempo;
la servilo "malsukcesas" kun OutOfMemory se la tasko postulas pretigon kaj ŝanĝi grandajn kvantojn da datenoj, kaj la ĉeesto de sinkronaj integriĝoj faras tre malfacila dividi pretigon en "pli malpezajn" transakciojn.
De arkitektura vidpunkto, la uzo de blokado de vokoj dum integriĝo kondukas al perdo de kontrolo pri la kvalito de individuaj subsistemoj: estas neeble certigi la celkvalitajn indikilojn de unu subsistemo izole de la kvalitindikiloj de alia subsistemo. Se subsistemoj estas evoluigitaj de malsamaj teamoj, tio estas granda problemo.
Aferoj fariĝas eĉ pli interesaj se la subsistemoj integriĝantaj estas en malsamaj aplikoj kaj vi devas fari sinkronajn ŝanĝojn ambaŭflanke. Kiel certigi la transakcon de ĉi tiuj ŝanĝoj?
Se ŝanĝoj estas faritaj en apartaj transakcioj, tiam vi devos provizi fidindan esceptan uzadon kaj kompenson, kaj ĉi tio tute forigas la ĉefan avantaĝon de sinkronaj integriĝoj - simpleco.
Distribuitaj transakcioj ankaŭ venas en menso, sed ni ne uzas ilin en niaj solvoj: estas malfacile certigi fidindecon.
"Sagao" kiel solvo al la transakcia problemo
Kun la kreskanta populareco de mikroservoj, la postulo por Sagao Ŝablono.
Ĉi tiu ŝablono perfekte solvas la supre menciitajn problemojn de longaj transakcioj, kaj ankaŭ pligrandigas la kapablojn de administrado de la stato de la sistemo de la flanko de komerca logiko: kompenso post malsukcesa transakcio eble ne retrovigas la sistemon al ĝia originala stato, sed provizas. alternativa datumtraktitinero. Ĉi tio ankaŭ ebligas al vi eviti ripeti sukcese finitajn datumtraktadpaŝojn kiam vi provas alporti la procezon al "bona" fino.
Kurioze, en monolitaj sistemoj ĉi tiu ŝablono ankaŭ estas grava kiam temas pri la integriĝo de loze kunligitaj subsistemoj kaj negativaj efikoj kaŭzitaj de longdaŭraj transakcioj kaj respondaj rimedseruroj estas observitaj.
Rilate al niaj komercaj procezoj en la BPM-stilo, rezultas tre facile efektivigi "Sagaojn": individuaj paŝoj de la "Sagao" povas esti specifitaj kiel agadoj ene de la komerca procezo, kaj ankaŭ la konstanta stato de la komerca procezo. determinas la internan staton de la "Sagao". Tio estas, ni ne postulas ajnan kroman kunordigan mekanismon. Vi nur bezonas mesaĝan brokeron, kiu subtenas "almenaŭ unufoje" garantiojn kiel transporton.
Sed ĉi tiu solvo ankaŭ havas sian propran "prezon":
komerca logiko fariĝas pli kompleksa: kompenso devas esti ellaborita;
necesos forlasi plenan konsistencon, kiu povas esti speciale sentema por monolitaj sistemoj;
La arkitekturo fariĝas iom pli komplika, kaj aperas plia bezono de mesaĝmakleristo;
aldonaj monitoraj kaj administraj iloj estos postulataj (kvankam ĝenerale tio estas bona: la kvalito de sistema servo pliiĝos).
Por monolitaj sistemoj, la pravigo por uzi "Sag" ne estas tiel evidenta. Por mikroservoj kaj aliaj SOA, kie plej verŝajne jam ekzistas makleristo, kaj plena konsistenco estas oferita ĉe la komenco de la projekto, la avantaĝoj de uzado de ĉi tiu ŝablono povas signife superi la malavantaĝojn, precipe se ekzistas oportuna API ĉe la komerca logiko. nivelo.
Enkapsuligante komercan logikon en mikroservoj
Kiam ni komencis eksperimenti kun mikroservoj, ekestis racia demando: kie meti la domajnan komercan logikon rilate al la servo, kiu certigas la persiston de domajnaj datumoj?
Rigardante la arkitekturon de diversaj BPMSoj, povas ŝajni racie apartigi komercan logikon de persisto: kreu tavolon de platformo kaj domajn-sendependaj mikroservoj, kiuj formas medion kaj ujon por ekzekuti domajnan komercan logikon, kaj desegni la persiston de domajnaj datumoj kiel aparta tavolo de tre simplaj kaj malpezaj mikroservoj. Komercaj procezoj ĉi-kaze plenumas instrumentadon de la servoj de la persista tavolo.
Ĉi tiu aliro havas tre grandan avantaĝon: vi povas pliigi la funkciecon de la platformo kiom vi volas, kaj nur la responda tavolo de platformaj mikroservoj fariĝos "grasa" de ĉi tio. Komercaj procezoj de iu ajn domajno tuj povas uzi la novan funkcion de la platformo tuj kiam ĝi estas ĝisdatigita.
Pli detala studo rivelis signifajn malavantaĝojn de ĉi tiu aliro:
platforma servo kiu efektivigas la komercan logikon de multaj domajnoj samtempe portas grandajn riskojn kiel ununura punkto de fiasko. Oftaj ŝanĝoj al komerca logiko pliigas la riskon de eraroj kondukantaj al tutsistemaj fiaskoj;
rendimentaj problemoj: komerca logiko funkcias kun siaj datumoj per mallarĝa kaj malrapida interfaco:
la datumoj denove estos kunigitaj kaj pumpitaj tra la reto-stako;
domajna servo ofte provizos pli da datenoj ol estas postulata por komerca logiko por procesi pro nesufiĉaj kapabloj por parametrigado de petoj sur la nivelo de la ekstera API de la servo;
pluraj sendependaj pecoj de komerca logiko povas plurfoje repeti la samajn datenojn por prilaborado (tiu problemo povas esti mildigita aldonante sesiokomponentojn kiuj kaŝmemorigas datenojn, sed tio plue malfaciligas la arkitekturon kaj kreas problemojn de datensignifeco kaj kaŝmemorinvalidigon);
transakciaj problemoj:
komercaj procezoj kun konstanta stato, kiu estas stokita de platforma servo, estas malkongruaj kun domajnaj datumoj, kaj ne estas facilaj manieroj solvi ĉi tiun problemon;
metado de domajna datumo blokado ekster la transakcio: se la domajna komerca logiko bezonas fari ŝanĝojn post unue kontroli la ĝustecon de la aktualaj datumoj, necesas ekskludi la eblecon de konkurenciva ŝanĝo en la prilaboritaj datumoj. Ekstera datuma blokado povas helpi solvi la problemon, sed tia solvo portas pliajn riskojn kaj reduktas la ĝeneralan fidindecon de la sistemo;
kromaj malfacilaĵoj dum ĝisdatigo: en iuj kazoj, la persista servo kaj komerca logiko devas esti ĝisdatigitaj sinkrone aŭ en strikta sinsekvo.
Finfine, ni devis reiri al bazaĵoj: enkapsuligi domajnan datumojn kaj domajnan komercan logikon en unu mikroservon. Ĉi tiu aliro simpligas la percepton de mikroservo kiel integra komponento de la sistemo kaj ne kaŭzas ĉi-suprajn problemojn. Ĉi tio ankaŭ ne estas donita senpage:
API-normigado estas postulata por interagado kun komerclogiko (aparte, por disponigi uzantajn agadojn kiel parto de komercprocezoj) kaj API-platformservoj; postulas pli zorgan atenton al API-ŝanĝoj, antaŭen kaj malantaŭen kongruo;
necesas aldoni pliajn rultempajn bibliotekojn por certigi la funkciadon de komerca logiko kiel parto de ĉiu tia mikroservo, kaj tio estigas novajn postulojn por tiaj bibliotekoj: malpezeco kaj minimumo de transitivaj dependecoj;
Komercaj logikprogramistoj devas kontroli bibliotekversiojn: se mikroservo ne estis finpretigita dum longa tempo, tiam ĝi plej verŝajne enhavos malmodernan version de la bibliotekoj. Ĉi tio povas esti neatendita malhelpo por aldoni novan funkcion kaj povas postuli migri la malnovan komercan logikon de tia servo al novaj versioj de bibliotekoj se estis nekongruaj ŝanĝoj inter versioj.
Tavolo de platformaj servoj ankaŭ ĉeestas en tia arkitekturo, sed ĉi tiu tavolo ne plu formas ujon por ekzekuti domajnan komercan logikon, sed nur ĝian medion, provizante helpajn "platformajn" funkciojn. Tia tavolo estas necesa ne nur por konservi la malpezan naturon de domajnaj mikroservoj, sed ankaŭ por centralizi administradon.
Ekzemple, uzantagadoj en komercaj procezoj generas taskojn. Tamen, laborante kun taskoj, la uzanto devas vidi taskojn de ĉiuj domajnoj en la ĝenerala listo, kio signifas, ke devas ekzisti responda platforma tasko registra servo, malplenigita de domajna komerca logiko. Konservi enkapsuligon de komerca logiko en tia kunteksto estas sufiĉe problema, kaj ĉi tio estas alia kompromiso de ĉi tiu arkitekturo.
Integriĝo de komercaj procezoj per la okuloj de programisto de aplikaĵo
Kiel menciite supre, aplikaĵprogramisto devas esti abstraktita de la teknikaj kaj inĝenieraj trajtoj de efektivigado de la interago de pluraj aplikoj por ke oni povu fidi je bona disvolva produktiveco.
Ni provu solvi iom malfacilan integrigan problemon, speciale elpensitan por la artikolo. Ĉi tio estos "luda" tasko implikanta tri aplikojn, kie ĉiu el ili difinas certan domajnan nomon: "app1", "app2", "app3".
Ene de ĉiu aplikaĵo, komercaj procezoj estas lanĉitaj, kiuj komencas "ludi pilkon" per la integriga buso. Mesaĝoj kun la nomo "Pilko" funkcios kiel pilko.
Reguloj de la ludo:
la unua ludanto estas la iniciatinto. Li invitas aliajn ludantojn al la ludo, komencas la ludon kaj povas fini ĝin iam ajn;
aliaj ludantoj deklaras sian partoprenon en la ludo, "konatiĝas" unu la alian kaj la unua ludanto;
post ricevado de la pilko, la ludanto elektas alian partoprenantan ludanton kaj pasas la pilkon al li. La tuta nombro de dissendoj estas kalkulita;
Ĉiu ludanto havas "energion" kiu malpliiĝas kun ĉiu enirpermesilo de la pilko de tiu ludanto. Kiam la energio finiĝas, la ludanto forlasas la ludon, sciigante sian eksiĝon;
se la ludanto restas sola, li tuj anoncas sian foriron;
Kiam ĉiuj ludantoj estas eliminitaj, la unua ludanto deklaras la ludon finita. Se li forlasas la ludon frue, li restas sekvi la ludon por kompletigi ĝin.
Por solvi ĉi tiun problemon, mi uzos nian DSL por komercaj procezoj, kiu ebligas al ni priskribi la logikon en Kotlin kompakte, kun minimumo de boilerplate.
La komerca procezo de la unua ludanto (alinome la iniciatinto de la ludo) funkcios en la aplikaĵo app1:
klaso 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}")
}
Krom ekzekutado de komerca logiko, ĉi-supra kodo povas produkti objektomodelon de komerca procezo, kiu povas esti bildigita en la formo de diagramo. Ni ankoraŭ ne efektivigis la bildilon, do ni devis pasigi iom da tempo desegnante (ĉi tie mi iomete simpligis la BPMN-notacion pri la uzo de pordegoj por plibonigi la konsekvencon de la diagramo kun la suba kodo):
app2 inkluzivos la komercan procezon de la alia ludanto:
klaso 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}")
}
Diagramo:
En la aplikaĵo app3 ni faros ludanton kun iomete malsama konduto: anstataŭ hazarde elekti la sekvan ludanton, li agos laŭ la cirkla-subskribolista algoritmo:
klaso 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}")
}
Alie, la konduto de la ludanto ne diferencas de la antaŭa, do la diagramo ne ŝanĝiĝas.
Nun ni bezonas teston por ruli ĉion ĉi. Mi donos nur la kodon de la testo mem, por ne malordigi la artikolon per kaldrono (fakte, mi uzis la testan medion kreitan pli frue por testi la integriĝon de aliaj komercaj procezoj):
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;
}
El ĉio ĉi ni povas tiri plurajn gravajn konkludojn:
kun la necesaj iloj, aplikaĵprogramistoj povas krei integrigajn interagojn inter aplikoj sen interrompi komercan logikon;
la komplekseco de integriga tasko kiu postulas inĝenieristikkompetentecojn povas esti kaŝita ene de la kadro se tio estas komence inkludita en la arkitekturo de la kadro. La malfacileco de problemo ne povas esti kaŝita, do la solvo de malfacila problemo en kodo aspektos kiel ĝi;
Dum evoluigado de integriĝlogiko, estas nepre konsideri eventualan konsistencon kaj la mankon de linearigigeblo de ŝanĝoj en la stato de ĉiuj integriĝpartoprenantoj. Ĉi tio devigas nin malfaciligi la logikon por igi ĝin malsentema al la ordo en kiu okazas eksteraj eventoj. En nia ekzemplo, la ludanto estas devigita partopreni en la ludo post kiam li deklaras sian eliron el la ludo: aliaj ludantoj daŭre transdonos la pilkon al li ĝis la informoj pri lia eliro atingos kaj estas prilaboritaj de ĉiuj partoprenantoj. Ĉi tiu logiko ne sekvas el la reguloj de la ludo kaj estas kompromisa solvo en la kadro de la elektita arkitekturo.
Poste, ni parolos pri la diversaj komplikaĵoj de nia solvo, kompromisoj kaj aliaj punktoj.
Ĉiuj mesaĝoj estas en unu vico
Ĉiuj integraj aplikoj funkcias kun unu integriga buso, kiu estas prezentita en la formo de ekstera makleristo, unu BPMQueue por mesaĝoj kaj unu BPMTopic temo por signaloj (okazaĵoj). Meti ĉiujn mesaĝojn tra unu atendovico estas mem kompromiso. Je la komerca logika nivelo, vi nun povas enkonduki tiom da novaj mesaĝtipoj kiom vi volas sen fari ŝanĝojn al la sistema strukturo. Ĉi tio estas signifa simpligo, sed ĝi portas certajn riskojn, kiuj en la kunteksto de niaj tipaj taskoj ne ŝajnis al ni tiom gravaj.
Tamen, estas unu subtileco ĉi tie: ĉiu aplikaĵo filtras "siajn" mesaĝojn de la vosto ĉe la enirejo, laŭ la nomo de sia domajno. La domajno ankaŭ povas esti specifita en signaloj se vi bezonas limigi la "amplekson de videbleco" de la signalo al unu ununura aplikaĵo. Ĉi tio devus pliigi la busan trairon, sed la komerca logiko devas nun funkcii kun domajnaj nomoj: por adresado de mesaĝoj - deviga, por signaloj - dezirinda.
Certigante Integrigan Busan Fidindecon
Fidindeco konsistas el pluraj punktoj:
La elektita mesaĝmakleristo estas kritika komponento de la arkitekturo kaj ununura punkto de fiasko: ĝi devas esti sufiĉe mistolerema. Vi devus uzi nur tempelprovitajn efektivigojn, kun bona subteno kaj granda komunumo;
necesas certigi altan haveblecon de la mesaĝmakleristo, por kiu ĝi devas esti fizike apartigita de la integraj aplikoj (alta havebleco de aplikoj kun aplikata komerca logiko estas multe pli malfacila kaj multekosta certigi);
la makleristo estas devigita provizi "almenaŭ unufoje" liverajn garantiojn. Ĉi tio estas deviga postulo por fidinda funkciado de la integriga buso. Ne necesas "precize unufoje" nivelaj garantioj: komercaj procezoj, kiel regulo, ne estas sentemaj al la ripeta alveno de mesaĝoj aŭ eventoj, kaj en specialaj taskoj, kie ĉi tio gravas, estas pli facile aldoni pliajn ĉekojn al la komerco. logiko ol konstante uzi sufiĉe "multekostajn" " garantiojn;
sendi mesaĝojn kaj signalojn devas esti implikita en ĝenerala transakcio kun ŝanĝoj en la stato de komercaj procezoj kaj domajnaj datumoj. La preferata opcio estus uzi ŝablonon Transakcia Elirkesto, sed ĝi postulos plian tabelon en la datumbazo kaj ripetilon. En JEE-aplikoj, tio povas esti simpligita uzante lokan JTA-manaĝeron, sed la ligo al la elektita makleristo devas povi funkcii en XA;
pritraktantoj de envenantaj mesaĝoj kaj eventoj ankaŭ devas labori kun transakcio, kiu ŝanĝas la staton de komerca procezo: se tia transakcio estas refunkciigita, tiam la ricevo de la mesaĝo devas esti nuligita;
mesaĝoj kiuj ne povis esti liveritaj pro eraroj devas esti konservitaj en aparta stokado D.L.Q. (Dead Letter Queue). Por ĉi tiu celo, ni kreis apartan platforman mikroservon, kiu stokas tiajn mesaĝojn en sia stokado, indeksas ilin laŭ atributoj (por rapida grupigo kaj serĉado), kaj elmontras API por vidi, resendi al la cela adreso kaj forigi mesaĝojn. Sistemadministrantoj povas labori kun ĉi tiu servo per sia retinterfaco;
en la agordoj de makleristo, vi devas ĝustigi la nombron da liverprovoj kaj prokrastoj inter liveroj por redukti la verŝajnecon de mesaĝoj enirantaj en DLQ (estas preskaŭ neeble kalkuli la optimumajn parametrojn, sed vi povas agi empirie kaj ĝustigi ilin dum operacio. );
La DLQ-butiko devas esti kontinue monitorita, kaj la monitora sistemo devas atentigi sistemadministrantojn tiel ke kiam neliveritaj mesaĝoj okazas, ili povas respondi kiel eble plej rapide. Ĉi tio reduktos la "trafitan areon" de fiasko aŭ komerca logika eraro;
la integriga buso devas esti nesentema al la provizora foresto de aplikaĵoj: abonoj al temo devas esti daŭraj, kaj la domajna nomo de la aplikaĵo devas esti unika tiel ke dum la aplikaĵo forestas, iu alia ne provos prilabori ĝiajn mesaĝojn de la aplikaĵo. vico.
Certigante fadenan sekurecon de komerca logiko
La sama kazo de komerca procezo povas ricevi plurajn mesaĝojn kaj eventojn samtempe, kies prilaborado komenciĝos paralele. Samtempe, por programisto de aplikaĵo, ĉio devus esti simpla kaj faden-sekura.
La komerca logiko de procezo prilaboras ĉiun eksteran okazaĵon kiu influas tiun komercprocezon individue. Tiaj eventoj povus esti:
lanĉante komercan procezon;
uzantagado rilata al agado ene de komerca procezo;
ricevo de mesaĝo aŭ signalo al kiu estas abonita komercproceza petskribo;
ekigado de tempigilo agordita de komercproceza petskribo;
kontrola ago per API (ekzemple, procezinterrompo).
Ĉiu tia evento povas ŝanĝi la staton de komerca procezo: iuj agadoj povas finiĝi kaj aliaj povas komenciĝi, kaj la valoroj de konstantaj propraĵoj povas ŝanĝiĝi. Fermi ajnan agadon povas rezultigi la aktivigon de unu aŭ pli el la sekvaj agadoj. Tiuj, siavice, povas ĉesi atendi aliajn eventojn aŭ, se ili ne bezonas pliajn datumojn, povas plenumi en la sama transakcio. Antaŭ fermi la transakcion, la nova stato de la komerca procezo estas konservita en la datumbazo, kie ĝi atendos la sekvan eksteran eventon.
Konstantaj komercprocezaj datumoj konservitaj en interrilata datumbazo estas tre oportuna punkto por sinkronigi prilaboradon se vi uzas SELECT FOR UPDATE. Se unu transakcio sukcesis akiri la staton de komerca procezo de la bazo por ŝanĝi ĝin, tiam neniu alia transakcio paralele povos akiri la saman staton por alia ŝanĝo, kaj post la kompletiĝo de la unua transakcio, la dua estas. garantiite ricevi la jam ŝanĝitan staton.
Uzante pesimismajn serurojn ĉe la flanko de DBMS, ni plenumas ĉiujn necesajn postulojn ACIDO, kaj ankaŭ reteni la kapablon skali la aplikaĵon kun komerca logiko pliigante la nombron da kurantaj petskriboj.
Tamen, pesimismaj seruroj minacas nin per blokadoj, kio signifas, ke SELECT FOR UPDATE devus ankoraŭ esti limigita al iu akceptebla tempoforigo, se okazos blokadoj en iuj aĉaj kazoj en la komerca logiko.
Alia problemo estas la sinkronigado de la komenco de komerca procezo. Dum ne ekzistas kazo de komerca procezo, ne ekzistas ŝtato en la datumbazo, do la priskribita metodo ne funkcios. Se vi bezonas certigi la unikecon de komerca procezo en specifa amplekso, tiam vi bezonos ian sinkronigan objekton asociitan kun la proceza klaso kaj la responda amplekso. Por solvi ĉi tiun problemon, ni uzas malsaman ŝlosan mekanismon, kiu ebligas al ni preni seruron sur arbitra rimedo specifita per ŝlosilo en formato URI per ekstera servo.
En niaj ekzemploj, la komerca procezo de InitialPlayer enhavas deklaron
uniqueConstraint = UniqueConstraints.singleton
Tial, la protokolo enhavas mesaĝojn pri prenado kaj liberigo de la seruro de la responda ŝlosilo. Ne ekzistas tiaj mesaĝoj por aliaj komercaj procezoj: uniqueConstraint ne estas agordita.
Problemoj de komercaj procezoj kun konstanta stato
Kelkfoje havi persistan staton ne nur helpas, sed ankaŭ vere malhelpas evoluon.
Problemoj komenciĝas kiam ŝanĝoj devas esti faritaj al la komerca logiko kaj/aŭ komercproceza modelo. Ne ĉiu tia ŝanĝo kongruas kun la malnova stato de komercaj procezoj. Se estas multaj vivaj okazoj en la datumbazo, tiam fari nekongruajn ŝanĝojn povas kaŭzi multajn problemojn, kiujn ni ofte renkontis dum uzado de jBPM.
Depende de la profundo de la ŝanĝoj, vi povas agi en du manieroj:
kreu novan komercprocezan tipon por ne fari nekongruajn ŝanĝojn al la malnova, kaj uzu ĝin anstataŭ la malnovan kiam oni lanĉas novajn petskribojn. Malnovaj kopioj daŭre funkcios "kiel antaŭe";
migri la konstantan staton de komercaj procezoj kiam ĝi ĝisdatigas komercan logikon.
La unua maniero estas pli simpla, sed havas siajn limojn kaj malavantaĝojn, ekzemple:
duobligo de komerca logiko en multaj komercprocezaj modeloj, pliigante la volumenon de komerca logiko;
Ofte necesas tuja transiro al nova komerca logiko (laŭ integrigaj taskoj - preskaŭ ĉiam);
la programisto ne scias, en kiu punkto malmodernaj modeloj povas esti forigitaj.
Praktike ni uzas ambaŭ alirojn, sed faris kelkajn decidojn por faciligi nian vivon:
En la datumbazo, la konstanta stato de komerca procezo estas konservita en facile legebla kaj facile prilaborita formo: en JSON-forma ĉeno. Ĉi tio permesas migradojn esti faritaj kaj ene de la aplikaĵo kaj ekstere. Kiel lasta rimedo, vi povas korekti ĝin permane (precipe utila en evoluo dum senararigado);
la integra komerca logiko ne uzas la nomojn de komercaj procezoj, tiel ke iam ajn eblas anstataŭigi la efektivigon de unu el la partoprenantaj procezoj per nova kun nova nomo (ekzemple, "InitialPlayerV2"). La ligado okazas per mesaĝ- kaj signalnomoj;
la procezmodelo havas version-numeron, kiun ni pliigas se ni faras nekongruajn ŝanĝojn al ĉi tiu modelo, kaj ĉi tiu nombro estas konservita kune kun la stato de la proceza petskribo;
la persista stato de la procezo estas legita el la datumbazo unue en oportunan objektomodelon, kun kiu la migra proceduro povas funkcii se la modelversionumero ŝanĝiĝis;
la migra proceduro estas metita apud la komerca logiko kaj estas nomita "maldiligenta" por ĉiu kazo de la komerca procezo en la momento de ĝia restarigo de la datumbazo;
se vi bezonas migri la staton de ĉiuj procezaj petskriboj rapide kaj sinkrone, pli klasikaj datumbazaj migradsolvoj estas uzataj, sed vi devas labori kun JSON.
Ĉu vi bezonas alian kadron por komercaj procezoj?
La solvoj priskribitaj en la artikolo permesis al ni signife simpligi nian vivon, vastigi la gamon de problemoj solvitaj ĉe la aplikaĵa disvolva nivelo kaj fari la ideon apartigi komercan logikon en mikroservojn pli alloga. Por atingi tion, multe da laboro estis farita, tre "malpeza" kadro por komercaj procezoj estis kreita, same kiel servaj komponantoj por solvi la identigitajn problemojn en la kunteksto de larĝa gamo de aplikaj problemoj. Ni deziras dividi ĉi tiujn rezultojn kaj fari la disvolviĝon de komunaj komponantoj malferma aliro sub libera permesilo. Ĉi tio postulos iom da penado kaj tempo. Kompreni la postulon pri tiaj solvoj povus esti plia instigo por ni. En la proponita artikolo, tre malmulte da atento estas donita al la kapabloj de la kadro mem, sed kelkaj el ili estas videblaj el la prezentitaj ekzemploj. Se ni ja publikigos nian kadron, aparta artikolo estos dediĉita al ĝi. Intertempe, ni dankus, se vi lasus iom da komentoj respondante la demandon:
Nur registritaj uzantoj povas partopreni la enketon. Ensaluti, bonvolu.
Ĉu vi bezonas alian kadron por komercaj procezoj?
18,8%Jes, mi delonge serĉas ion tian
12,5%Mi interesiĝas lerni pli pri via efektivigo, ĝi povus esti utila2
6,2%Ni uzas unu el la ekzistantaj kadroj, sed pensas pri anstataŭigi1
18,8%Ni uzas unu el la ekzistantaj kadroj, ĉio estas en ordo3