Интеграција БПМ стила

Интеграција БПМ стила

Здраво, Һабр!

Наша компанија је специјализована за развој софтверских решења ЕРП класе, чији лавовски део заузимају трансакциони системи са огромном количином пословне логике и тока докумената а ла ЕДМС. Тренутне верзије наших производа су засноване на ЈаваЕЕ технологијама, али ми такође активно експериментишемо са микроуслугама. Једна од најпроблематичнијих области оваквих решења је интеграција различитих подсистема који припадају суседним доменима. Проблеми интеграције су нам увек задавали огромну главобољу, без обзира на архитектонске стилове, технолошке стекове и оквире које користимо, али у последње време постоји напредак у решавању таквих проблема.

У чланку на који вам скрећем пажњу говорићу о искуству и архитектонским истраживањима које НПО Криста има у назначеном простору. Такође ћемо погледати пример једноставног решења проблема интеграције са становишта програмера апликације и открити шта се крије иза ове једноставности.

Одрицање од одговорности

Архитектонска и техничка решења описана у чланку предлажем ја на основу личног искуства у контексту конкретних задатака. Ова решења не тврде да су универзална и можда неће бити оптимална под другим условима употребе.

Какве везе БПМ има са тим?

Да бисмо одговорили на ово питање, потребно је да се удубимо мало дубље у специфичности примењених проблема наших решења. Главни део пословне логике у нашем типичном трансакционом систему је уношење података у базу података преко корисничког интерфејса, ручна и аутоматизована верификација ових података, провођење кроз неки радни ток, објављивање у другом систему / аналитичкој бази / архиви, генерисање извештаја . Дакле, кључна функција система за купце је аутоматизација њихових интерних пословних процеса.

Ради практичности, у комуникацији користимо термин „документ” као неку апстракцију скупа података уједињених заједничким кључем са којим се одређени ток посла може „повезати”.
Али шта је са логиком интеграције? На крају крајева, задатак интеграције је генерисан архитектуром система, који се „сече“ на делове НЕ на захтев купца, већ под утицајем потпуно различитих фактора:

  • подлеже Конвејевом закону;
  • као резултат поновног коришћења подсистема претходно развијених за друге производе;
  • по нахођењу архитекте, на основу нефункционалних захтева.

Постоји велико искушење да се логика интеграције одвоји од пословне логике главног тока посла, како се не би загадила пословна логика артефактима интеграције и растеретио програмера апликације потребе да се удубљује у специфичности архитектонског пејзажа система. Овај приступ има низ предности, али пракса показује његову неефикасност:

  • решавање проблема интеграције се обично враћа на најједноставније опције у облику синхроних позива због ограничених тачака проширења у имплементацији главног тока посла (недостаци синхроне интеграције су разматрани у наставку);
  • артефакти интеграције и даље продиру у основну пословну логику када је потребна повратна информација из другог подсистема;
  • програмер апликације игнорише интеграцију и може је лако прекинути променом тока посла;
  • систем престаје да буде јединствена целина са тачке гледишта корисника, „шавови“ између подсистема постају приметни, а појављују се сувишне корисничке операције које иницирају пренос података из једног подсистема у други.

Други приступ је разматрање интеграцијских интеракција као интегралног дела основне пословне логике и тока посла. Да би се спречило да квалификације програмера апликација нагло порасту, креирање нових интеракција интеграције треба да буде лако и без напора, са минималном могућношћу избора решења. Ово је теже учинити него што се чини: алат мора бити довољно моћан да кориснику пружи потребну разноликост опција за његову употребу, а да му не дозволи да „пуца себи у ногу“. Постоје многа питања на која инжењер мора да одговори у контексту задатака интеграције, али о којима програмер апликације не би требало да размишља у свом свакодневном раду: границе трансакција, доследност, атомичност, безбедност, скалирање, дистрибуција оптерећења и ресурса, рутирање, маршалирање, дистрибуцију и пребацивање контекста итд. Потребно је понудити програмерима апликација прилично једноставне шаблоне решења у којима се већ крију одговори на сва оваква питања. Ови шаблони морају бити прилично сигурни: пословна логика се веома често мења, што повећава ризик од уношења грешака, цена грешака мора остати на прилично ниском нивоу.

Али какве везе БПМ има са тим? Постоји много опција за имплементацију тока посла...
Заиста, још једна имплементација пословних процеса је веома популарна у нашим решењима – кроз декларативно дефинисање дијаграма транзиције стања и повезивање руковаоца са пословном логиком за транзиције. У овом случају, стање које одређује тренутну позицију „документа“ у пословном процесу је атрибут самог „документа“.

Интеграција БПМ стила
Овако изгледа процес на почетку пројекта

Популарност ове имплементације је због релативне једноставности и брзине креирања линеарних пословних процеса. Међутим, како софтверски системи стално постају сложенији, аутоматизовани део пословног процеса расте и постаје све сложенији. Постоји потреба за декомпозицијом, поновним коришћењем делова процеса, као и процесима гранања тако да се свака грана извршава паралелно. У таквим условима алат постаје незгодан, а дијаграм прелаза стања губи свој информациони садржај (интеракције интеграције се уопште не одражавају на дијаграму).

Интеграција БПМ стила
Овако изгледа процес након неколико итерација појашњења захтева.

Излаз из ове ситуације била је интеграција мотора јБПМ у неке производе са најсложенијим пословним процесима. Краткорочно, ово решење је имало успеха: постало је могуће имплементирати сложене пословне процесе уз одржавање прилично информативног и релевантног дијаграма у нотацији. БПМН2.

Интеграција БПМ стила
Мали део сложеног пословног процеса

Дугорочно, решење није оправдало очекивања: висок радни интензитет креирања пословних процеса помоћу визуелних алата није омогућио постизање прихватљивих показатеља продуктивности, а сам алат је постао један од најомиљенијих међу програмерима. Било је и притужби на унутрашњу структуру мотора, што је довело до појаве многих „закрпа“ и „штака“.

Главни позитиван аспект коришћења јБПМ-а била је свест о предностима и штетностима постојаног стања инстанце пословног процеса. Такође смо видели могућност коришћења процесног приступа за имплементацију сложених протокола интеграције између различитих апликација користећи асинхроне интеракције путем сигнала и порука. Присуство постојаног стања игра кључну улогу у томе.

На основу наведеног, можемо закључити: Процесни приступ у БПМ стилу нам омогућава да решимо широк спектар задатака за аутоматизацију све сложенијих пословних процеса, хармонично уклапање интеграционих активности у ове процесе и одржавање могућности визуелног приказа имплементираног процеса у одговарајућој нотацији.

Недостаци синхроних позива као обрасца интеграције

Синхрона интеграција се односи на најједноставнији позив за блокирање. Један подсистем делује као серверска страна и излаже АПИ са потребним методом. Други подсистем делује као страна клијента и у право време обавља позив и чека резултат. У зависности од архитектуре система, страна клијента и сервера могу се налазити или у истој апликацији и процесу, или у различитим. У другом случају, потребно је да примените неку РПЦ имплементацију и обезбедите маршалирање параметара и резултата позива.

Интеграција БПМ стила

Овај образац интеграције има прилично велики скуп недостатака, али се због своје једноставности веома широко користи у пракси. Брзина имплементације плени и приморава вас да је користите изнова и изнова у условима хитних рокова, евидентирајући решење као технички дуг. Али такође се дешава да га неискусни програмери користе несвесно, једноставно не схватајући негативне последице.

Поред најочигледнијег повећања повезаности подсистема, постоје и мање очигледни проблеми са „растућим“ и „протежним“ трансакцијама. Заиста, ако пословна логика направи неке промене, онда се трансакције не могу избећи, а трансакције, заузврат, блокирају одређене ресурсе апликације на које утичу ове промене. То јест, све док један подсистем не сачека одговор од другог, неће моћи да заврши трансакцију и да уклони браве. Ово значајно повећава ризик од разних ефеката:

  • Губи се одзивност система, корисници дуго чекају на одговоре на упите;
  • сервер генерално престаје да одговара на захтеве корисника због пренатрпаног пула нити: већина нити је закључана на ресурсу који заузима трансакција;
  • Почињу да се појављују застоји: вероватноћа њиховог појављивања снажно зависи од трајања трансакције, количине пословне логике и закључавања укључених у трансакцију;
  • појављују се грешке временског ограничења трансакције;
  • сервер „не успе“ са ОутОфМемори ако задатак захтева обраду и промену великих количина података, а присуство синхроних интеграција отежава поделу обраде на „лакше“ трансакције.

Са архитектонске тачке гледишта, коришћење блокирања позива током интеграције доводи до губитка контроле над квалитетом појединих подсистема: немогуће је обезбедити циљне индикаторе квалитета једног подсистема изоловано од индикатора квалитета другог подсистема. Ако подсистеме развијају различити тимови, то је велики проблем.

Ствари постају још интересантније ако су подсистеми који се интегришу у различитим апликацијама и морате да извршите синхроне промене на обе стране. Како обезбедити трансационалност ових промена?

Ако се промене врше у одвојеним трансакцијама, онда ћете морати да обезбедите поуздано руковање изузетцима и компензацију, а то у потпуности елиминише главну предност синхроних интеграција – једноставност.

Дистрибуиране трансакције такође падају на памет, али их не користимо у нашим решењима: тешко је обезбедити поузданост.

„Сага“ као решење проблема трансакције

Са растућом популарношћу микроуслуга, потражња за Сага Паттерн.

Овај образац савршено решава горе поменуте проблеме дугих трансакција, а такође проширује могућности управљања стањем система са стране пословне логике: компензација након неуспеле трансакције можда неће вратити систем у првобитно стање, али ће обезбедити алтернативни пут обраде података. Ово вам такође омогућава да избегнете понављање успешно завршених корака обраде података када покушавате да доведете процес до „доброг“ краја.

Занимљиво је да је у монолитним системима овај образац такође релевантан када је у питању интеграција слабо повезаних подсистема и примећују се негативни ефекти изазвани дуготрајним трансакцијама и одговарајућим закључавањем ресурса.

У односу на наше пословне процесе у БПМ стилу, показало се да је веома лако имплементирати „Саге“: појединачни кораци „Саге“ се могу навести као активности у оквиру пословног процеса, а трајно стање пословног процеса такође одређује унутрашње стање „Саге“. Односно, не треба нам никакав додатни механизам координације. Све што вам треба је посредник за поруке који подржава гаранције „барем једном“ као транспорт.

Али ово решење такође има своју „цену“:

  • пословна логика постаје сложенија: компензацију треба разрадити;
  • биће неопходно напустити пуну доследност, што може бити посебно осетљиво за монолитне системе;
  • Архитектура постаје мало компликованија и појављује се додатна потреба за брокером порука;
  • биће потребни додатни алати за праћење и администрацију (иако је генерално ово добро: квалитет системске услуге ће се повећати).

За монолитне системе, оправдање за коришћење "Саг" није тако очигледно. За микросервисе и друге СОА, где највероватније већ постоји брокер, а пуна доследност је жртвована на почетку пројекта, предности коришћења овог обрасца могу значајно да превазиђу недостатке, посебно ако постоји погодан АПИ у пословној логици ниво.

Енкапсулација пословне логике у микросервисима

Када смо почели да експериментишемо са микросервисима, поставило се разумно питање: где сместити пословну логику домена у односу на сервис који обезбеђује постојаност података домена?

Када се посматра архитектура различитих БПМС-ова, може изгледати разумно одвојити пословну логику од постојаности: креирати слој микросервиса независних од платформе и домена који формирају окружење и контејнер за извршавање пословне логике домена и дизајнирати постојаност података домена као посебан слој веома једноставних и лаганих микросервиса. Пословни процеси у овом случају обављају оркестрацију услуга слоја постојаности.

Интеграција БПМ стила

Овај приступ има веома велику предност: можете повећати функционалност платформе колико год желите, а само ће одговарајући слој платформских микросервиса постати „дебео“ од овога. Пословни процеси из било ког домена могу одмах да користе нову функционалност платформе чим се она ажурира.

Детаљнија студија је открила значајне недостатке овог приступа:

  • услуга платформе која истовремено извршава пословну логику многих домена носи велике ризике као једну тачку неуспеха. Честе промене пословне логике повећавају ризик од грешака које доводе до кварова у целом систему;
  • проблеми са перформансама: пословна логика ради са својим подацима кроз уски и спор интерфејс:
    • подаци ће поново бити распоређени и пумпани кроз мрежни стек;
    • услуга домена ће често обезбедити више података него што је потребно за обраду пословне логике због недовољних могућности за параметрирање захтева на нивоу екстерног АПИ-ја услуге;
    • неколико независних делова пословне логике може више пута поново захтевати исте податке за обраду (овај проблем се може ублажити додавањем компоненти сесије које кеширају податке, али то додатно компликује архитектуру и ствара проблеме релевантности података и поништавања кеша);
  • проблеми са трансакцијама:
    • пословни процеси са постојаним стањем, које чува сервис платформе, нису у складу са подацима домена и не постоје лаки начини за решавање овог проблема;
    • постављање блокирања података домена ван трансакције: уколико пословна логика домена треба да изврши измене након прве провере исправности актуелних података, потребно је искључити могућност конкурентске промене у обрађеним подацима. Блокирање екстерних података може помоћи у решавању проблема, али такво решење носи додатне ризике и смањује укупну поузданост система;
  • додатне потешкоће при ажурирању: у неким случајевима, сервис постојаности и пословна логика морају да се ажурирају синхроно или у строгом редоследу.

На крају, морали смо да се вратимо основама: енкапсулирати податке домена и пословну логику домена у једну микросервис. Овај приступ поједностављује перцепцију микросервиса као интегралне компоненте система и не изазива горе наведене проблеме. Ово се такође не даје бесплатно:

  • АПИ стандардизација је потребна за интеракцију са пословном логиком (посебно за пружање активности корисника као део пословних процеса) и услугама АПИ платформе; захтева већу пажњу на промене АПИ-ја, компатибилност унапред и уназад;
  • неопходно је додати додатне рунтиме библиотеке да би се обезбедило функционисање пословне логике као део сваког таквог микросервиса, а то доводи до нових захтева за такве библиотеке: лакоћа и минимум транзитивних зависности;
  • Програмери пословне логике треба да прате верзије библиотека: ако микросервис није финализован дуже време, онда ће највероватније садржати застарелу верзију библиотека. Ово може бити неочекивана препрека за додавање нове функције и може захтевати миграцију старе пословне логике такве услуге у нове верзије библиотека ако је било некомпатибилних промена између верзија.

Интеграција БПМ стила

У таквој архитектури је присутан и слој платформских услуга, али овај слој више не чини контејнер за извршавање пословне логике домена, већ само његово окружење, пружајући помоћне функције „платформе“. Такав слој је неопходан не само да би се одржала лагана природа микроуслуга домена, већ и да се централизује управљање.

На пример, активности корисника у пословним процесима генеришу задатке. Међутим, када ради са задацима, корисник мора да види задатке са свих домена на општој листи, што значи да мора постојати одговарајућа платформска услуга регистрације задатака, очишћена од пословне логике домена. Одржавање инкапсулације пословне логике у таквом контексту је прилично проблематично, а ово је још један компромис ове архитектуре.

Интеграција пословних процеса очима програмера апликација

Као што је горе поменуто, програмер апликације мора бити апстрахован од техничких и инжењерских карактеристика имплементације интеракције неколико апликација тако да се може рачунати на добру развојну продуктивност.

Хајде да покушамо да решимо прилично тежак проблем интеграције, посебно измишљен за чланак. Ово ће бити задатак „игре“ који укључује три апликације, при чему свака од њих дефинише одређени назив домена: „апп1“, „апп2“, „апп3“.

Унутар сваке апликације покрећу се пословни процеси који почињу да „играју лопту“ преко интеграционе магистрале. Поруке са именом „лопта“ ће деловати као лопта.

Правила игре:

  • први играч је иницијатор. Он позива друге играче у игру, почиње игру и може је завршити у било ком тренутку;
  • остали играчи се изјашњавају о учешћу у игри, „упознају“ једни друге и првог играча;
  • након што прими лопту, играч бира другог играча који учествује и додаје му лопту. Укупан број преноса се рачуна;
  • Сваки играч има "енергију" која се смањује са сваким додавањем лопте тог играча. Када енергија понестане, играч напушта игру, најављујући своју оставку;
  • ако играч остане сам, одмах најављује одлазак;
  • Када су сви играчи елиминисани, први играч проглашава игру завршеном. Ако рано напусти игру, остаје му да прати игру да би је завршио.

Да бих решио овај проблем, користићу наш ДСЛ за пословне процесе, који нам омогућава да опишемо логику у Котлину компактно, са минимумом шаблона.

Пословни процес првог играча (познатог као покретач игре) ће радити у апликацији апп1:

цласс ИнитиалПлаиер

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

Поред извршавања пословне логике, горњи код може да произведе објектни модел пословног процеса, који се може визуелно приказати у облику дијаграма. Још увек нисмо имплементирали визуелизатор, па смо морали да потрошимо мало времена на његово цртање (овде сам мало поједноставио БПМН нотацију у вези са употребом капија да побољшам конзистентност дијаграма са датим кодом):

Интеграција БПМ стила

апп2 ће укључивати пословни процес другог играча:

цласс РандомПлаиер

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

дијаграм:

Интеграција БПМ стила

У апликацији апп3 направићемо играча са нешто другачијим понашањем: уместо да насумично бира следећег играча, он ће се понашати према кружном алгоритму:

цласс РоундРобинПлаиер

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

Иначе, понашање играча се не разликује од претходног, па се дијаграм не мења.

Сада нам је потребан тест да бисмо све ово покренули. Даћу само код самог теста, како не бих затрпао чланак шаблоном (у ствари, користио сам претходно креирано окружење за тестирање да тестирам интеграцију других пословних процеса):

тестГаме()

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

Покренимо тест и погледајмо дневник:

излаз конзоле

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

Из свега овога можемо извући неколико важних закључака:

  • са потребним алатима, програмери апликација могу креирати интеграцијске интеракције између апликација без прекидања пословне логике;
  • сложеност интеграционог задатка који захтева инжењерске компетенције може се сакрити унутар оквира ако је то иницијално укључено у архитектуру оквира. Тежина проблема се не може сакрити, па ће решење тешког проблема у коду изгледати тако;
  • Приликом развоја логике интеграције неопходно је водити рачуна о евентуалној конзистентности и недостатку линеаризабилности промена стања свих учесника интеграције. Ово нас приморава да закомпликујемо логику како бисмо је учинили неосетљивим на редослед у коме се дешавају спољашњи догађаји. У нашем примеру, играч је принуђен да учествује у игри након што прогласи свој излазак из игре: други играчи ће наставити да му додају лопту све док информација о његовом изласку не стигне и не буде обрађена од стране свих учесника. Ова логика не произилази из правила игре и представља компромисно решење у оквиру одабране архитектуре.

Затим ћемо причати о разним замршеностима нашег решења, компромисима и другим тачкама.

Све поруке су у једном реду

Све интегрисане апликације раде са једном интеграционом магистралом, која је представљена у облику екстерног брокера, једне БПМКуеуе за поруке и једне БПМТопиц теме за сигнале (догађаје). Стављање свих порука у један ред је само по себи компромис. На нивоу пословне логике, сада можете увести онолико нових типова порука колико желите, а да не правите промене у структури система. Ово је значајно поједностављење, али носи одређене ризике, који нам у контексту наших типичних задатака нису изгледали толико значајни.

Интеграција БПМ стила

Међутим, овде постоји једна суптилност: свака апликација филтрира „своје” поруке из реда на улазу, по имену свог домена. Домен се такође може навести у сигналима ако је потребно да ограничите „обим видљивости“ сигнала на једну апликацију. Ово би требало да повећа пропусност магистрале, али пословна логика сада мора да ради са именима домена: за адресирање порука - обавезно, за сигнале - пожељно.

Обезбеђивање поузданости интеграционе магистрале

Поузданост се састоји од неколико тачака:

  • Изабрани посредник порука је критична компонента архитектуре и једина тачка квара: мора бити довољно толерантан на грешке. Требало би да користите само временски тестиране имплементације, са добром подршком и великом заједницом;
  • потребно је обезбедити високу доступност брокера порука, за шта он мора бити физички одвојен од интегрисаних апликација (високу доступност апликација са примењеном пословном логиком је много теже и скупље обезбедити);
  • посредник је дужан да обезбеди „барем једном” гаранције испоруке. Ово је обавезан захтев за поуздан рад интеграционе магистрале. Нема потребе за гаранцијама нивоа „тачно једном”: пословни процеси по правилу нису осетљиви на поновљене пристизања порука или догађаја, а у посебним задацима где је то важно лакше је додати додатне провере у пословање. логике него да стално користите прилично „скупе“ „ гаранције;
  • слање порука и сигнала мора бити укључено у укупну трансакцију са променама стања пословних процеса и података о домену. Пожељна опција би била употреба шаблона Трансацтионал Оутбок, али ће бити потребна додатна табела у бази података и репетитор. У ЈЕЕ апликацијама, ово се може поједноставити коришћењем локалног ЈТА менаџера, али веза са изабраним брокером мора да функционише у XA;
  • руковаоци долазних порука и догађаја такође морају да раде са трансакцијом која мења стање пословног процеса: ако се таква трансакција врати, онда се пријем поруке мора отказати;
  • поруке које нису могле да се испоруче због грешака морају се чувати у посебном складишту ДЛК (Ред мртвих писама). У ту сврху смо креирали посебну платформу микросервис која чува такве поруке у свом складишту, индексира их по атрибутима (за брзо груписање и претрагу) и излаже АПИ за преглед, поновно слање на одредишну адресу и брисање порука. Администратори система могу да раде са овом услугом преко свог веб интерфејса;
  • у подешавањима брокера, потребно је да прилагодите број покушаја испоруке и кашњења између испорука како бисте смањили вероватноћу да поруке уђу у ДЛК (скоро је немогуће израчунати оптималне параметре, али можете деловати емпиријски и прилагодити их током рада );
  • ДЛК продавница мора бити континуирано надгледана, а систем за надзор мора да упозори системске администраторе како би могли да реагују што је брже могуће када се појаве неиспоручене поруке. Ово ће смањити „погођено подручје“ грешке или грешке пословне логике;
  • интеграциона магистрала мора бити неосетљива на привремено одсуство апликација: претплате на тему морају бити трајне, а име домена апликације мора бити јединствено тако да док је апликација одсутна, неко други неће покушавати да обради њене поруке са куеуе.

Обезбеђивање сигурности нити пословне логике

Иста инстанца пословног процеса може примити неколико порука и догађаја одједном, чија ће обрада почети паралелно. У исто време, за програмера апликације, све би требало да буде једноставно и безбедно за нити.

Пословна логика процеса обрађује сваки спољашњи догађај који утиче на тај пословни процес појединачно. Такви догађаји могу бити:

  • покретање инстанце пословног процеса;
  • радња корисника везана за активност у оквиру пословног процеса;
  • пријем поруке или сигнала на који је инстанца пословног процеса претплаћена;
  • активирање тајмера постављеног од стране инстанце пословног процеса;
  • контролна акција преко АПИ-ја (на пример, прекид процеса).

Сваки такав догађај може да промени стање инстанце пословног процеса: неке активности могу да се заврше, а друге да почну, а вредности трајних својстава могу да се промене. Затварање било које активности може довести до активирања једне или више од следећих активности. Они, заузврат, могу престати да чекају на друге догађаје или, ако им нису потребни додатни подаци, могу да заврше у истој трансакцији. Пре затварања трансакције, ново стање пословног процеса се чува у бази података, где ће чекати да се догоди следећи екстерни догађај.

Трајни подаци пословног процеса ускладиштени у релационој бази података су веома погодна тачка за синхронизацију обраде ако користите СЕЛЕЦТ ФОР УПДАТЕ. Ако је једна трансакција успела да добије стање пословног процеса из базе за његову промену, онда ниједна друга паралелна трансакција неће моћи да добије исто стање за другу промену, а по завршетку прве трансакције, друга је гарантовано да прими већ измењено стање.

Користећи песимистичке браве на страни ДБМС-а, испуњавамо све потребне захтеве КИСЕЛИНА, а такође задржавају могућност скалирања апликације са пословном логиком повећањем броја покренутих инстанци.

Међутим, песимистична закључавања нам прете застојима, што значи да СЕЛЕЦТ ФОР УПДАТЕ и даље треба да буде ограничен на неко разумно временско ограничење у случају да дође до застоја у неким критичним случајевима у пословној логици.

Други проблем је синхронизација почетка пословног процеса. Док не постоји инстанца пословног процеса, не постоји стање у бази података, тако да описани метод неће радити. Ако треба да обезбедите јединственост инстанце пословног процеса у одређеном опсегу, онда ће вам бити потребна нека врста синхронизационог објекта који је повезан са класом процеса и одговарајућим опсегом. Да бисмо решили овај проблем, користимо другачији механизам закључавања који нам омогућава да закључамо произвољни ресурс наведен кључем у УРИ формату преко спољне услуге.

У нашим примерима, ИнитиалПлаиер пословни процес садржи декларацију

uniqueConstraint = UniqueConstraints.singleton

Дакле, дневник садржи поруке о преузимању и отпуштању браве одговарајућег кључа. Не постоје такве поруке за друге пословне процесе: уникуеЦонстраинт није подешен.

Проблеми пословних процеса са постојаним стањем

Понекад постојано стање не само да помаже, већ и заиста омета развој.
Проблеми почињу када је потребно извршити промене у пословној логици и/или моделу пословног процеса. Није свака таква промена компатибилна са старим стањем пословних процеса. Ако постоји много живих инстанци у бази података, онда уношење некомпатибилних измена може изазвати много проблема, на које смо често наилазили када користимо јБПМ.

У зависности од дубине промена, можете деловати на два начина:

  1. креирајте нови тип пословног процеса како не бисте правили некомпатибилне измене старог и користите га уместо старог при покретању нових инстанци. Стари примерци ће наставити да раде „као и раније“;
  2. мигрира трајно стање пословних процеса приликом ажурирања пословне логике.

Први начин је једноставнији, али има своја ограничења и недостатке, на пример:

  • дуплирање пословне логике у многим моделима пословних процеса, повећање обима пословне логике;
  • Често је потребан тренутни прелазак на нову пословну логику (у погледу задатака интеграције – скоро увек);
  • програмер не зна у ком тренутку се застарели модели могу избрисати.

У пракси користимо оба приступа, али смо донели низ одлука да нам олакшају живот:

  • У бази података, трајно стање пословног процеса се чува у лако читљивом и лако обрађеном облику: у стрингу формата ЈСОН. Ово омогућава обављање миграција и унутар апликације и споља. Као последње средство, можете га исправити ручно (нарочито корисно у развоју током отклањања грешака);
  • интеграцијска пословна логика не користи називе пословних процеса, тако да је у сваком тренутку могуће заменити имплементацију једног од процеса који учествују новим са новим именом (на пример, „ИнитиалПлаиерВ2“). Везивање се дешава преко имена порука и сигнала;
  • модел процеса има број верзије, који повећавамо ако направимо некомпатибилне измене у овом моделу, и овај број се чува заједно са стањем инстанце процеса;
  • трајно стање процеса се прво чита из базе података у погодан објектни модел, са којим процедура миграције може да ради ако се променио број верзије модела;
  • процедура миграције се поставља поред пословне логике и назива се „лењи“ за сваку инстанцу пословног процеса у тренутку његовог враћања из базе података;
  • ако треба брзо и синхроно да мигрирате стање свих инстанци процеса, користе се класичнија решења за миграцију базе података, али морате да радите са ЈСОН-ом.

Да ли вам је потребан други оквир за пословне процесе?

Решења описана у чланку су нам омогућила да значајно поједноставимо свој живот, проширимо спектар питања која се решавају на нивоу развоја апликација и учинимо привлачнијом идеју раздвајања пословне логике на микросервисе. Да би се то постигло, урађено је много посла, креиран је веома „лаки“ оквир за пословне процесе, као и сервисне компоненте за решавање идентификованих проблема у контексту широког спектра апликативних проблема. Имамо жељу да поделимо ове резултате и учинимо развој заједничких компоненти отвореним приступом под слободном лиценцом. Ово ће захтевати мало труда и времена. Разумевање потражње за оваквим решењима могло би да нам буде додатни подстицај. У предложеном чланку врло је мало пажње посвећено могућностима самог оквира, али су неке од њих видљиве из приказаних примера. Ако ипак објавимо наш оквир, биће му посвећен посебан чланак. У међувремену, били бисмо вам захвални ако оставите малу повратну информацију тако што ћете одговорити на питање:

Само регистровани корисници могу учествовати у анкети. Пријавите се, Добродошао си.

Да ли вам је потребан други оквир за пословне процесе?

  • 100%Да, дуго сам тражио овако нешто

  • 100%Заинтересован сам да сазнам више о вашој имплементацији, могло би бити корисно2

  • 100%Користимо један од постојећих оквира, али размишљамо о замени1

  • 100%Користимо један од постојећих оквира, све је у реду3

  • 100%сналазимо се без оквира3

  • 100%напиши своје4

Гласало је 16 корисника. Уздржано је било 7 корисника.

Извор: ввв.хабр.цом

Додај коментар