BPM-stila integriĝo

BPM-stila integriĝo

Saluton, Habr!

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.

BPM-stila integriĝo
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).

BPM-stila integriĝo
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.

BPM-stila integriĝo
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.

BPM-stila integriĝo

Ĉ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.

BPM-stila integriĝo

Ĉ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.

BPM-stila integriĝo

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):

BPM-stila integriĝo

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:

BPM-stila integriĝo

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;
}

Ni faru la teston kaj rigardu la protokolon:

konzola eligo

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

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.

BPM-stila integriĝo

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:

  1. 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";
  2. 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

  • 18,8%ni administras sen kadro3

  • 25,0%skribu vian4

16 uzantoj voĉdonis. 7 uzantoj sindetenis.

fonto: www.habr.com

Aldoni komenton