BPM-stijl integratie

BPM-stijl integratie

Hello, Habr!

Ons bedrijf is gespecialiseerd in het ontwikkelen van softwareoplossingen van ERP-klasse, waarvan het grootste deel bestaat uit transactiesystemen met een enorme hoeveelheid bedrijfslogica en documentenstroom à la ECM. Huidige versies van onze producten zijn gebaseerd op JavaEE-technologieën, maar we experimenteren ook actief met microservices. Een van de meest problematische aspecten van dergelijke oplossingen is de integratie van verschillende subsystemen die verband houden met aangrenzende domeinen. Integratietaken vormen voor ons altijd al een groot probleem, ongeacht de architectuurstijlen, technologiestacks en frameworks die we gebruiken. Maar onlangs is er vooruitgang geboekt bij het oplossen van dergelijke problemen.

In het artikel dat u wordt aangeboden, vertel ik u over de ervaring en het architectonisch onderzoek van NPO Krista in het aangegeven gebied. We zullen ook een voorbeeld van een eenvoudige oplossing voor een integratieprobleem vanuit het perspectief van een applicatieontwikkelaar bekijken en ontdekken wat er achter deze eenvoud schuilgaat.

Disclaimer

De in het artikel beschreven architectonische en technische oplossingen stel ik voor op basis van mijn eigen ervaring in de context van specifieke opdrachten. Deze oplossingen pretenderen niet universeel te zijn en zijn mogelijk niet optimaal onder andere gebruiksomstandigheden.

Wat heeft BPM ermee te maken?

Om deze vraag te beantwoorden, moeten we wat dieper ingaan op de specifieke toepassingen van onze oplossingen. Het belangrijkste onderdeel van de bedrijfslogica in een typisch transactiesysteem is het invoeren van gegevens in de database via gebruikersinterfaces, het handmatig en automatisch verifiëren van deze gegevens, het door een bepaalde workflow leiden ervan, het publiceren ervan naar een ander systeem/een andere analytische database/een ander archief en het genereren van rapporten. De belangrijkste functie van het systeem voor klanten is daarom de automatisering van hun interne bedrijfsprocessen.

Voor het gemak gebruiken we in de communicatie de term ‘document’ als een bepaalde abstractie van een set gegevens die door een gemeenschappelijke sleutel is verenigd en waaraan een bepaalde workflow kan worden ‘gekoppeld’.
Maar hoe zit het met de integratielogica? De integratietaak wordt immers gegenereerd door de systeemarchitectuur, die NIET op verzoek van de klant, maar onder invloed van geheel andere factoren, in delen wordt ‘geknipt’:

  • onder de werking van de wet van Conway;
  • als resultaat van het hergebruiken van subsystemen die eerder voor andere producten zijn ontwikkeld;
  • volgens de beslissing van de architect, op basis van niet-functionele vereisten.

De verleiding is groot om de integratielogica te scheiden van de bedrijfslogica van de hoofdworkflow. Zo voorkom je dat de bedrijfslogica vervuild raakt met integratieartefacten en dat de applicatieontwikkelaar zich hoeft te verdiepen in de specifieke kenmerken van het architectuurlandschap van het systeem. Deze aanpak heeft een aantal voordelen, maar de praktijk wijst uit dat deze niet effectief is:

  • de oplossing van integratietaken komt meestal neer op de eenvoudigste opties in de vorm van synchrone aanroepen vanwege de beperkte uitbreidingspunten in de implementatie van de hoofdworkflow (de nadelen van synchrone integratie worden hieronder besproken);
  • integratieartefacten dringen nog steeds door tot de kernlogica van het bedrijf wanneer feedback van een ander subsysteem nodig is;
  • de applicatieontwikkelaar negeert de integratie en kan deze gemakkelijk verbreken door de workflow te wijzigen;
  • Vanuit het perspectief van de gebruiker is het systeem niet langer één geheel, er worden 'naden' tussen subsystemen zichtbaar en er ontstaan ​​redundante handelingen van de gebruiker die de overdracht van gegevens van het ene subsysteem naar het andere initiëren.

Een andere aanpak is om integratie-interacties te beschouwen als een integraal onderdeel van de belangrijkste bedrijfslogica en workflow. Om te voorkomen dat de vaardigheden van applicatieontwikkelaars de pan uit rijzen, moet het creëren van nieuwe integratie-interacties eenvoudig en moeiteloos zijn, met minimale opties voor het kiezen van de oplossingsmethode. Dat is moeilijker dan het lijkt: de tool moet krachtig genoeg zijn om de gebruiker de benodigde verscheidenheid aan use cases te bieden, zonder dat ze zichzelf 'in de voet schieten'. Er zijn veel vragen waar een engineer antwoord op moet geven in het kader van integratietaken, maar waar een applicatieontwikkelaar in zijn dagelijkse werk niet over na hoeft te denken: transactiegrenzen, consistentie, atomiciteit, beveiliging, schaalbaarheid, verdeling van belasting en bronnen, routing, marshalling, distributie en contextswitching, enzovoort. We moeten applicatieontwikkelaars relatief eenvoudige oplossingsjablonen bieden waarin de antwoorden op al dit soort vragen al verborgen zitten. Deze sjablonen moeten voldoende veilig zijn: de bedrijfslogica verandert zeer vaak, waardoor het risico op fouten toeneemt. De kosten van fouten moeten laag genoeg blijven.

Maar wat heeft BPM hiermee te maken? Er zijn veel opties voor het implementeren van workflow...
Een andere implementatie van bedrijfsprocessen is inderdaad erg populair in onze oplossingen: via de declaratieve definitie van een toestandsovergangsdiagram en de koppeling van handlers met bedrijfslogica aan overgangen. In dit geval is de status die de huidige positie van het ‘document’ in het bedrijfsproces bepaalt, een kenmerk van het ‘document’ zelf.

BPM-stijl integratie
Zo ziet het proces eruit aan het begin van een project

De populariteit van deze implementatie is te danken aan de relatieve eenvoud en snelheid waarmee lineaire bedrijfsprocessen kunnen worden gecreëerd. Naarmate softwaresystemen complexer worden, groeit ook het geautomatiseerde deel van het bedrijfsproces en wordt het complexer. Er is behoefte aan decompositie, hergebruik van procesonderdelen en vertakking van processen, zodat elke vertakking parallel wordt uitgevoerd. Onder dergelijke omstandigheden wordt de tool onhandig en verliest het toestandsovergangsdiagram zijn informatieve waarde (integratie-interacties worden helemaal niet in het diagram weerspiegeld).

BPM-stijl integratie
Zo ziet het proces eruit nadat de eisen meerdere keren zijn verfijnd.

De oplossing voor deze situatie was de integratie van de motor jBPM in sommige producten met de meest complexe bedrijfsprocessen. Op korte termijn had deze oplossing enig succes: het werd mogelijk om complexe bedrijfsprocessen te implementeren en toch een voldoende informatief en actueel diagram in de notatie te behouden. BPMN2.

BPM-stijl integratie
Een klein onderdeel van een complex bedrijfsproces

Op de lange termijn voldeed de oplossing niet aan de verwachtingen: het creëren van bedrijfsprocessen met behulp van visuele tools was erg arbeidsintensief, waardoor het niet mogelijk was om acceptabele productiviteitsindicatoren te behalen. Bovendien werd de tool zelf een van de minst geliefde tools onder ontwikkelaars. Er werden ook klachten ingediend over de interne structuur van de motor, wat leidde tot het ontstaan ​​van veel ‘patches’ en ‘krukken’.

Het belangrijkste positieve aspect van het gebruik van jBPM was het inzicht in de voor- en nadelen van het hebben van een eigen, permanente status voor een bedrijfsprocesinstantie. We zagen ook de mogelijkheid om een ​​procesgebaseerde aanpak te gebruiken voor de implementatie van complexe integratieprotocollen tussen verschillende applicaties met behulp van asynchrone interacties via signalen en berichten. De aanwezigheid van een persistente toestand speelt hierbij een essentiële rol.

Op basis van het bovenstaande kunnen we het volgende concluderen: De procesbenadering in de BPM-stijl maakt het mogelijk om een ​​breed scala aan taken op het gebied van de automatisering van steeds complexere bedrijfsprocessen op te lossen, integratieactiviteiten op harmonieuze wijze in deze processen te integreren en tegelijkertijd de mogelijkheid te behouden om het geïmplementeerde proces visueel weer te geven in een daarvoor geschikte notatie.

Nadelen van synchrone oproepen als integratiepatroon

Synchrone integratie verwijst naar de eenvoudigste blokkerende oproep. Eén subsysteem fungeert als serverkant en stelt een API beschikbaar met de vereiste methode. Een ander subsysteem fungeert als clientzijde en voert op het juiste moment een aanroep uit en wacht op het resultaat. Afhankelijk van de systeemarchitectuur kunnen de client- en serverzijde zich in dezelfde toepassing en hetzelfde proces bevinden, of in verschillende. In het tweede geval is het noodzakelijk om een ​​RPC-implementatie te gebruiken en te zorgen voor marshalling van parameters en het aanroepresultaat.

BPM-stijl integratie

Dit integratiepatroon kent een behoorlijk aantal nadelen, maar wordt in de praktijk veel gebruikt vanwege de eenvoud ervan. De snelheid van de implementatie is boeiend en zorgt ervoor dat je het steeds opnieuw toepast, zelfs in omstandigheden van ‘brandende’ deadlines, waarbij de oplossing in technische schuld wordt vastgelegd. Maar het komt ook voor dat onervaren ontwikkelaars het onbewust gebruiken en zich niet realiseren welke negatieve gevolgen het heeft.

Naast de meest voor de hand liggende toename in connectiviteit van subsystemen, zijn er ook minder voor de hand liggende problemen met 'uitdijing' en 'uitrekken' van transacties. Als de bedrijfslogica inderdaad wijzigingen aanbrengt, dan zijn transacties essentieel. Transacties blokkeren op hun beurt bepaalde toepassingsbronnen die door deze wijzigingen worden beïnvloed. Dat wil zeggen dat het ene subsysteem pas de transactie kan voltooien en de vergrendelingen kan verwijderen als het op een antwoord van het andere subsysteem wacht. Hierdoor wordt het risico op verschillende effecten aanzienlijk vergroot:

  • het systeem reageert niet meer goed, gebruikers moeten lang wachten op reacties op verzoeken;
  • de server reageert helemaal niet meer op verzoeken van gebruikers vanwege een overvolle threadpool: de meeste threads blijven hangen in het vergrendelen van een resource die in beslag wordt genomen door een transactie;
  • Er beginnen deadlocks te ontstaan: de waarschijnlijkheid dat deze ontstaan ​​hangt sterk af van de duur van de transacties, de hoeveelheid bedrijfslogica en de locks die bij de transactie betrokken zijn;
  • Er verschijnen transactie-time-outfouten;
  • de server crasht vanwege OutOfMemory als de taak het verwerken en wijzigen van grote hoeveelheden data vereist, en de aanwezigheid van synchrone integraties maakt het erg moeilijk om de verwerking op te splitsen in "lichtere" transacties.

Vanuit een architectonisch oogpunt leidt het gebruik van blokkerende aanroepen tijdens de integratie tot een verlies van kwaliteitscontrole van afzonderlijke subsystemen: het is onmogelijk om de beoogde kwaliteitsindicatoren van één subsysteem te garanderen zonder de kwaliteitsindicatoren van een ander subsysteem te isoleren. Als subsystemen door verschillende teams worden ontwikkeld, is dit een groot probleem.

Het wordt nog interessanter als de te integreren subsystemen zich in verschillende toepassingen bevinden en er aan beide kanten gelijktijdig wijzigingen moeten worden doorgevoerd. Hoe kunnen we ervoor zorgen dat deze veranderingen transactioneel zijn?

Als er wijzigingen worden aangebracht in afzonderlijke transacties, dan is een robuuste uitzonderingsbehandeling en compensatie vereist. Hierdoor wordt het grootste voordeel van synchrone integraties, namelijk eenvoud, volledig tenietgedaan.

Gedistribueerde transacties komen ook in gedachten, maar die gebruiken we niet in onze oplossingen: het is moeilijk om de betrouwbaarheid ervan te garanderen.

"Saga" als oplossing voor het transactieprobleem

Met de groeiende populariteit van microservices is er een toenemende vraag Saga-patroon.

Dit patroon lost de hierboven genoemde problemen met betrekking tot lange transacties op perfecte wijze op en breidt bovendien de mogelijkheden voor het beheren van de systeemstatus vanuit de bedrijfslogica uit: compensatie na een mislukte transactie brengt het systeem mogelijk niet terug naar de oorspronkelijke status, maar biedt wel een alternatieve route voor de gegevensverwerking. Hiermee voorkomt u ook dat succesvol voltooide gegevensverwerkingsstappen opnieuw moeten worden uitgevoerd wanneer u het proces opnieuw tot een 'goed' einde wilt brengen.

Interessant genoeg is dit patroon in monolithische systemen ook relevant bij de integratie van losjes gekoppelde subsystemen. Lange transacties en de bijbehorende resourcevergrendelingen hebben dan negatieve effecten.

In relatie tot onze BPM-stijl bedrijfsprocessen blijkt het implementeren van “Saga’s” heel eenvoudig: individuele “Saga”-stappen kunnen worden gedefinieerd als activiteiten binnen een bedrijfsproces, en de persistente status van een bedrijfsproces bepaalt onder andere de interne status van de “Saga”. Dat wil zeggen dat we geen extra coördinatiemechanisme nodig hebben. Het enige dat u nodig hebt is een berichtenbroker die als transport de garantie "ten minste één keer" ondersteunt.

Maar deze oplossing heeft ook een eigen “prijs”:

  • De bedrijfslogica wordt complexer: compensatie moet worden verwerkt;
  • het zal nodig zijn om de volledige consistentie te verlaten, wat vooral gevoelig kan zijn bij monolithische systemen;
  • de architectuur wordt iets complexer en er is extra behoefte aan een berichtenmakelaar;
  • Er zijn aanvullende hulpmiddelen voor monitoring en beheer nodig (hoewel dit over het algemeen zelfs goed is: de kwaliteit van de systeemdienstverlening zal verbeteren).

Voor monolithische systemen is de rechtvaardiging voor het gebruik van "Sag" niet zo duidelijk. Voor microservices en andere SOA's waarbij er waarschijnlijk al een broker is en volledige consistentie bij aanvang van het project is opgeofferd, kunnen de voordelen van dit patroon aanzienlijk opwegen tegen de nadelen. Dit geldt vooral als er een handige API op het niveau van de bedrijfslogica is.

Bedrijfslogica in microservices inkapselen

Toen we begonnen te experimenteren met microservices, rees een terechte vraag: waar moeten we de domeinlogica plaatsen ten opzichte van de service die de persistentie van domeingegevens waarborgt?

Wanneer u de architectuur van verschillende BPMS bekijkt, lijkt het wellicht verstandig om bedrijfslogica te scheiden van persistentie. Maak een laag met platform- en domeinonafhankelijke microservices die een omgeving en container vormen voor het uitvoeren van domeinbedrijfslogica. Implementeer persistentie van domeingegevens in een aparte laag met zeer eenvoudige en lichtgewicht microservices. In dit geval voeren bedrijfsprocessen de orkestratie van persistentielaagservices uit.

BPM-stijl integratie

Deze aanpak heeft een heel groot voordeel: je kunt de functionaliteit van het platform zo veel uitbreiden als je wilt, en alleen de bijbehorende laag met platform-microservices wordt hier 'dik' van. Bedrijfsprocessen uit elk domein kunnen direct gebruikmaken van de nieuwe platformfunctionaliteit zodra deze is bijgewerkt.

Uit een meer gedetailleerd onderzoek bleek dat deze aanpak aanzienlijke tekortkomingen kent:

  • Een platformservice die bedrijfslogica voor meerdere domeinen tegelijk uitvoert, brengt grote risico's met zich mee als single point of failure. Regelmatige wijzigingen in de bedrijfslogica vergroten het risico op fouten die tot systeembrede storingen leiden;
  • Prestatieproblemen: de bedrijfslogica werkt met zijn gegevens via een smalle en trage interface:
    • de gegevens worden opnieuw verzameld en door de netwerkstack gepompt;
    • de domeinservice retourneert vaak meer gegevens dan de bedrijfslogica nodig heeft voor verwerking, als gevolg van onvoldoende queryparameteriseringsmogelijkheden op het niveau van de externe API-service;
    • meerdere onafhankelijke stukjes bedrijfslogica kunnen herhaaldelijk dezelfde gegevens voor verwerking opvragen (dit probleem kan worden opgelost door sessiecomponenten toe te voegen die gegevens cachen, maar dit compliceert de architectuur verder en creëert problemen met de relevantie van de gegevens en cache-invalidatie);
  • transactionele kwesties:
    • Bedrijfsprocessen met een permanente status, die door de platformservice wordt opgeslagen, komen niet overeen met de domeingegevens. Hiervoor bestaan ​​geen eenvoudige oplossingen;
    • verplaatsen van domeingegevens die buiten de transactie worden vergrendeld: als de bedrijfslogica van het domein wijzigingen moet aanbrengen na eerst de juistheid van de huidige gegevens te hebben gecontroleerd, is het noodzakelijk om de mogelijkheid van gelijktijdige wijzigingen in de verwerkte gegevens uit te sluiten. Externe gegevensblokkering kan het probleem helpen oplossen, maar een dergelijke oplossing brengt extra risico's met zich mee en vermindert de algehele betrouwbaarheid van het systeem;
  • Extra complicaties tijdens het bijwerken: in sommige gevallen moeten de persistentieservice en de bedrijfslogica synchroon of in een strikte volgorde worden bijgewerkt.

Uiteindelijk moesten we terug naar de basis: domeingegevens en domeinbedrijfslogica samenvoegen in één microservice. Deze aanpak vereenvoudigt de perceptie van een microservice als integraal onderdeel van een systeem en leidt niet tot de hierboven genoemde problemen. Ook dit is niet gratis:

  • standaardisatie van API's voor interactie met bedrijfslogica (met name om gebruikersactiviteiten als onderdeel van bedrijfsprocessen te waarborgen) en API-platformservices is vereist; Er is meer aandacht nodig voor API-wijzigingen en voorwaartse en achterwaartse compatibiliteit;
  • het is noodzakelijk om extra runtime-bibliotheken toe te voegen om de werking van de bedrijfslogica binnen elke dergelijke microservice te garanderen. Dit stelt nieuwe eisen aan dergelijke bibliotheken: lichtgewicht en met een minimum aan transitieve afhankelijkheden;
  • Ontwikkelaars van bedrijfslogica moeten de bibliotheekversies in de gaten houden: als een microservice lange tijd niet is bijgewerkt, bevat deze hoogstwaarschijnlijk een verouderde versie van de bibliotheken. Dit kan een onverwacht obstakel vormen bij het toevoegen van een nieuwe functie. Het kan dan nodig zijn om de oude bedrijfslogica van een dergelijke service te migreren naar nieuwe versies van de bibliotheken als er incompatibele wijzigingen zijn tussen de versies.

BPM-stijl integratie

In een dergelijke architectuur is ook een laag platformdiensten aanwezig, maar deze laag vormt niet langer een container voor de uitvoering van domeinbedrijfslogica, maar slechts de omgeving ervan, die aanvullende 'platform'-functies levert. Een dergelijke laag is niet alleen nodig om het lichtgewicht karakter van domeinmicroservices te behouden, maar ook om het beheer te centraliseren.

Gebruikersactiviteiten in bedrijfsprocessen genereren bijvoorbeeld taken. Bij het werken met taken moet de gebruiker echter taken uit alle domeinen in een gemeenschappelijke lijst kunnen zien. Dat betekent dat er een overeenkomstige platformtaakregistratieservice moet zijn, zonder de bedrijfslogica van het domein. Het handhaven van de inkapseling van bedrijfslogica in een dergelijke context is behoorlijk problematisch en dit is een ander compromis van deze architectuur.

Integratie van bedrijfsprocessen door de ogen van een applicatieontwikkelaar

Zoals hierboven al is aangegeven, moet de applicatieontwikkelaar abstractie maken van de technische en ingenieurstechnische aspecten van het implementeren van de interactie tussen meerdere applicaties, om te kunnen rekenen op een goede ontwikkelingsproductiviteit.

Laten we proberen een tamelijk moeilijk integratieprobleem op te lossen, dat speciaal voor dit artikel is bedacht. Dit zal een "spel"-taak zijn waarbij drie applicaties betrokken zijn, waarbij elk van hen een bepaalde domeinnaam definieert: "app1", "app2", "app3".

Binnen elke applicatie worden bedrijfsprocessen opgestart en beginnen deze via de integratiebus met elkaar te 'spelen'. De bal wordt weergegeven door berichten met de naam "Bal".

Regels van het spel:

  • De eerste speler is de initiatiefnemer. Hij nodigt andere spelers uit voor het spel, start het spel en kan het op elk moment beëindigen;
  • andere spelers kondigen hun deelname aan het spel aan, ‘leren’ elkaar en de eerste speler kennen;
  • Nadat de speler de bal heeft ontvangen, kiest hij een andere deelnemende speler en passeert de bal naar hem. Het totale aantal transmissies wordt geteld;
  • Elke speler heeft "energie" die afneemt bij elke pass van de bal door die speler. Wanneer de energie op is, verlaat de speler het spel en geeft daarmee aan dat hij/zij met pensioen gaat;
  • indien een speler alleen gelaten wordt, kondigt hij onmiddellijk zijn ontslag aan;
  • Wanneer alle spelers zijn afgehaakt, verklaart de eerste speler het spel afgelopen. Als hij het spel vroegtijdig verlaat, dan blijft hij het spel volgen tot het is afgelopen.

Om dit probleem op te lossen, zal ik onze DSL voor bedrijfsprocessen gebruiken, waarmee u de logica in Kotlin op een compacte manier kunt beschrijven, met een minimum aan boilerplate-informatie.

De app1-applicatie zal het bedrijfsproces van de eerste speler uitvoeren (hij is tevens de initiator van het spel):

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

Naast het uitvoeren van bedrijfslogica kan de gegeven code een objectmodel van het bedrijfsproces produceren, dat als diagram kan worden gevisualiseerd. We hebben de visualizer nog niet geïmplementeerd, dus moesten we wat tijd besteden aan het tekenen ervan (hier heb ik de BPMN-notatie enigszins vereenvoudigd door gebruik te maken van poorten om de consistentie van het diagram met de meegeleverde code te verbeteren):

BPM-stijl integratie

De app2-applicatie zal het bedrijfsproces van een andere speler bevatten:

klasse RandomPlayer

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

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

class PlayersList: ArrayList<PlayerInfo>()

class RandomPlayer : ProcessImpl<RandomPlayer>(randomPlayerModel) {

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

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

val randomPlayerModel = processModel<RandomPlayer>(name = "RandomPlayer", 
                                                   version = 1) {

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

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

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

private fun MessageSendInstance<RandomPlayer, Int>.selectNextPlayer() {
    val player = if (process.players.isNotEmpty()) 
        process.players.random() 
    else 
        process.selfPlayer
    receiverDomain = player.domain
    receiverProcessInstanceId = player.id
    println("Step ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

Diagram:

BPM-stijl integratie

In de app3-applicatie maken we een speler met een iets ander gedrag: in plaats van willekeurig de volgende speler te selecteren, zal hij handelen volgens het round-robin-algoritme:

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

Verder is het gedrag van de speler niet anders dan in de vorige, dus het diagram verandert niet.

Nu hebben we een test nodig om dit allemaal uit te voeren. Ik zal alleen de code van de test zelf verstrekken, om het artikel niet te vol te gooien met een standaardtekstbestand (ik heb trouwens een eerder gemaakte testomgeving gebruikt om de integratie van andere bedrijfsprocessen te testen):

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

We voeren de test uit en bekijken het logbestand:

console-uitvoer

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

Uit dit alles kunnen enkele belangrijke conclusies worden getrokken:

  • Met de juiste hulpmiddelen kunnen applicatieontwikkelaars integratie-interacties tussen applicaties creëren zonder af te wijken van de bedrijfslogica;
  • De complexiteit van een integratietaak waarvoor technische competenties vereist zijn, kan in een raamwerk verborgen blijven als deze initieel in de architectuur van het raamwerk is ingebouwd. De moeilijkheidsgraad van een probleem kan niet worden verborgen. De oplossing voor een moeilijk probleem in de code zal er dan ook zo uitzien;
  • Bij het ontwikkelen van integratielogica is het van essentieel belang om rekening te houden met de uiteindelijke consistentie en het gebrek aan linearisatie van veranderingen in de toestand van alle integratiedeelnemers. Dit dwingt ons om de logica ingewikkelder te maken, zodat deze ongevoelig wordt voor de volgorde waarin externe gebeurtenissen plaatsvinden. In ons voorbeeld wordt de speler gedwongen om deel te nemen aan het spel, nadat hij heeft aangekondigd dat hij zich heeft teruggetrokken. Andere spelers blijven de bal naar hem passen totdat de informatie over zijn terugtrekking door alle deelnemers is verwerkt. Deze logica volgt niet uit de spelregels en is een compromisoplossing binnen het kader van de gekozen architectuur.

Vervolgens bespreken we de verschillende subtiliteiten van onze beslissing, compromissen en andere punten.

Alle berichten staan ​​in één wachtrij

Alle geïntegreerde applicaties werken met één integratiebus, die wordt gepresenteerd in de vorm van een externe broker, één BPMQueue-wachtrij voor berichten en één BPMTopic-onderwerp voor signalen (gebeurtenissen). Het verwerken van alle berichten via één wachtrij is op zichzelf al een compromis. Op het niveau van de bedrijfslogica kunt u nu zoveel nieuwe berichttypen introduceren als u wilt, zonder dat u wijzigingen in de systeemstructuur hoeft aan te brengen. Dit is een belangrijke vereenvoudiging, maar het brengt bepaalde risico's met zich mee die wij in de context van de taken die wij doorgaans uitvoeren, niet zo groot achten.

BPM-stijl integratie

Er is hier echter één subtiliteit: elke applicatie filtert ‘zijn’ berichten uit de wachtrij bij de ingang, op basis van de naam van zijn domein. Het domein kan ook in signalen worden gespecificeerd als het nodig is om de ‘scope’ van het signaal te beperken tot één enkele toepassing. Dit zou de busdoorvoer moeten verhogen, maar de bedrijfslogica moet nu met domeinnamen werken: voor bericht-adressering is dit verplicht, voor signalen is dit wenselijk.

Zorgen voor de betrouwbaarheid van de integratiebus

Betrouwbaarheid bestaat uit verschillende factoren:

  • De gekozen berichtenbroker is een cruciaal onderdeel van de architectuur en een single point of failure: deze moet voldoende fouttolerant zijn. Gebruik alleen beproefde implementaties met goede ondersteuning en een grote community;
  • het is noodzakelijk om een ​​hoge beschikbaarheid van de berichtenbroker te garanderen, waarvoor deze fysiek gescheiden moet zijn van de geïntegreerde applicaties (het is veel moeilijker en duurder om een ​​hoge beschikbaarheid van applicaties met toegepaste bedrijfslogica te garanderen);
  • De makelaar is verplicht om “minstens éénmalig” leveringsgaranties te verstrekken. Dit is een verplichte vereiste voor een betrouwbare werking van de integratiebus. Er is geen behoefte aan garanties die precies één keer plaatsvinden: bedrijfsprocessen zijn doorgaans niet gevoelig voor herhaalde ontvangst van berichten of gebeurtenissen, en bij speciale taken waar dit belangrijk is, is het gemakkelijker om extra controles aan de bedrijfslogica toe te voegen dan voortdurend vrij "dure" garanties te gebruiken;
  • Het verzenden van berichten en signalen moet betrokken zijn bij de algehele transactie met wijzigingen in de status van bedrijfsprocessen en domeingegevens. De voorkeursoptie zou zijn om een ​​patroon te gebruiken Transactionele Outbox, maar hiervoor zijn een extra tabel in de database en een repeater nodig. In JEE-toepassingen kan dit worden vereenvoudigd door gebruik te maken van een lokale JTA-manager, maar de verbinding met de geselecteerde broker moet in de modus kunnen werken XA;
  • inkomende berichten- en gebeurtenis-handlers moeten ook met de transactie voor het wijzigen van de status van het bedrijfsproces werken: als een dergelijke transactie wordt teruggedraaid, moet de ontvangst van het bericht ook worden geannuleerd;
  • Berichten die vanwege fouten niet konden worden afgeleverd, moeten in een aparte opslag worden opgeslagen D.L.Q. (Wachtrij voor dode brieven). Voor dit doel hebben we een aparte platform-microservice gecreëerd die dergelijke berichten opslaat, ze indexeert op basis van kenmerken (voor snelle groepering en zoekopdrachten) en een API beschikbaar stelt voor het bekijken, opnieuw verzenden naar het bestemmingsadres en verwijderen van berichten. Systeembeheerders kunnen met deze service werken via hun webinterface;
  • In de brokerinstellingen moet u het aantal herhaalde bezorgpogingen en de vertragingen tussen bezorgingen aanpassen om de kans te verkleinen dat berichten in de DLQ terechtkomen (het is vrijwel onmogelijk om de optimale parameters te berekenen, maar u kunt empirisch te werk gaan en ze tijdens de werking aanpassen).
  • De DLQ-repository moet voortdurend worden bewaakt en het bewakingssysteem moet systeembeheerders waarschuwen, zodat ze zo snel mogelijk kunnen reageren als er berichten onbestelbaar zijn. Hierdoor wordt de ‘schadezone’ van de resulterende storing of fout in de bedrijfslogica verkleind;
  • De integratiebus mag niet gevoelig zijn voor de tijdelijke afwezigheid van applicaties: onderwerpabonnementen moeten duurzaam zijn en de domeinnaam van de applicatie moet uniek zijn, zodat niemand anders de berichten uit de wachtrij probeert te verwerken zolang de applicatie afwezig is.

Zorgen voor threadveiligheid van bedrijfslogica

Hetzelfde bedrijfsproces kan meerdere berichten en gebeurtenissen tegelijk ontvangen, waarvan de verwerking parallel wordt gestart. Tegelijkertijd moet voor een applicatieontwikkelaar alles eenvoudig en thread-safe zijn.

De bedrijfslogica van een proces verwerkt elke externe gebeurtenis die van invloed is op dat bedrijfsproces afzonderlijk. Dergelijke gebeurtenissen kunnen zijn:

  • een bedrijfsprocesinstantie starten;
  • een gebruikersactie gerelateerd aan een activiteit binnen een bedrijfsproces;
  • ontvangst van een bericht of signaal waarop een bedrijfsprocesinstantie is geabonneerd;
  • het activeren van een timer die is ingesteld door een bedrijfsprocesinstantie;
  • besturingsactie via API (bijv. procesafbreken).

Elke dergelijke gebeurtenis kan de status van een bedrijfsprocesinstantie wijzigen: sommige activiteiten kunnen eindigen en andere kunnen beginnen, de waarden van persistente eigenschappen kunnen veranderen. Als u een activiteit sluit, kunnen er een of meer volgende activiteiten worden geactiveerd. Zij kunnen op hun beurt niet langer wachten op andere gebeurtenissen of, als ze geen aanvullende gegevens nodig hebben, de transactie gewoon afronden. Voordat de transactie wordt gesloten, wordt de nieuwe status van het bedrijfsproces opgeslagen in de database, waar het wacht op de volgende externe gebeurtenis.

Blijvende bedrijfsprocesgegevens die in een relationele database zijn opgeslagen, vormen een zeer handig synchronisatiepunt voor verwerking bij gebruik van SELECT FOR UPDATE. Als één transactie erin slaagt om de status van een bedrijfsproces uit de database op te halen om deze te wijzigen, dan kan geen enkele andere transactie tegelijkertijd dezelfde status opvragen voor een andere wijziging. Na voltooiing van de eerste transactie is het dan ook gegarandeerd dat de tweede transactie de reeds gewijzigde status ontvangt.

Door gebruik te maken van pessimistische sloten aan de DBMS-zijde voldoen we aan alle noodzakelijke vereisten. ACIDen behoudt u ook de mogelijkheid om de applicatie te schalen met bedrijfslogica door het aantal actieve instanties te verhogen.

Pessimistische vergrendelingen vormen echter een bedreiging voor deadlocks. Dit betekent dat SELECT FOR UPDATE nog steeds beperkt moet worden door een redelijke time-out voor het geval er deadlocks ontstaan ​​in ernstige gevallen in de bedrijfslogica.

Een ander probleem is de synchronisatie van de start van het bedrijfsproces. Zolang er geen exemplaar van een bedrijfsproces bestaat, is er geen status ervan in de database aanwezig en zal de beschreven methode niet werken. Als u de uniciteit van een bedrijfsprocesinstantie binnen een bepaalde scope wilt garanderen, hebt u een synchronisatieobject nodig dat is gekoppeld aan de procesklasse en de bijbehorende scope. Om dit probleem op te lossen, gebruiken we een ander vergrendelingsmechanisme waarmee we een vergrendeling kunnen aanbrengen op een willekeurige bron die wordt gespecificeerd door een sleutel in URI-formaat via een externe service.

In onze voorbeelden bevat het InitialPlayer-bedrijfsproces een declaratie

uniqueConstraint = UniqueConstraints.singleton

Daarom bevat het logboek meldingen over het openen en sluiten van het slot op de bijbehorende sleutel. Er zijn geen dergelijke berichten voor andere bedrijfsprocessen: uniqueConstraint is niet ingesteld.

Problemen met bedrijfsprocessen met persistente status

Soms is een aanhoudende toestand niet alleen nuttig, maar belemmert het de ontwikkeling ook echt.
Problemen ontstaan ​​wanneer er wijzigingen in de bedrijfslogica en/of het bedrijfsprocesmodel moeten worden aangebracht. Niet elke verandering is compatibel met de oude bedrijfsprocessen. Als er veel "live" instanties in de database staan, kunnen er incompatibele wijzigingen worden aangebracht die voor veel problemen zorgen. Dat kwamen we vaak tegen bij het gebruik van jBPM.

Afhankelijk van de diepte van de wijzigingen zijn er twee manieren om verder te gaan:

  1. Maak een nieuw bedrijfsprocestype aan om te voorkomen dat er incompatibele wijzigingen in het oude type worden aangebracht, en gebruik dit in plaats van het oude wanneer u nieuwe instanties start. Oude kopieën blijven "op de oude manier" werken;
  2. migreer de persistente status van bedrijfsprocessen bij het bijwerken van de bedrijfslogica.

De eerste manier is eenvoudiger, maar kent zijn beperkingen en nadelen, bijvoorbeeld:

  • duplicatie van bedrijfslogica in veel bedrijfsprocesmodellen, toename van het volume aan bedrijfslogica;
  • een onmiddellijke overgang naar nieuwe bedrijfslogica is vaak vereist (in termen van integratietaken – bijna altijd);
  • De ontwikkelaar weet niet op welk punt het mogelijk is om verouderde modellen te verwijderen.

In de praktijk passen we beide benaderingen toe, maar we hebben een aantal beslissingen genomen om ons leven gemakkelijker te maken:

  • In de database wordt de permanente status van het bedrijfsproces opgeslagen in een vorm die gemakkelijk te lezen en te verwerken is: in een JSON-formaatstring. Hierdoor kunnen migraties zowel binnen als buiten de applicatie worden uitgevoerd. In extreme gevallen kunt u het handmatig corrigeren (vooral handig tijdens het debuggen tijdens de ontwikkeling);
  • De integratielogica maakt geen gebruik van bedrijfsprocesnamen, waardoor het op elk gewenst moment mogelijk is om de implementatie van een van de deelnemende processen te vervangen door een nieuw proces met een nieuwe naam (bijvoorbeeld 'InitialPlayerV2'). De binding vindt plaats via de namen van berichten en signalen;
  • het procesmodel heeft een versienummer dat we verhogen als we incompatibele wijzigingen aan het model aanbrengen. Dit nummer wordt samen met de status van het procesexemplaar opgeslagen;
  • de persistente status van het proces wordt eerst uit de database gelezen in een handig objectmodel, waarmee de migratieprocedure kan werken als het versienummer van het model is gewijzigd;
  • de migratieprocedure wordt naast de bedrijfslogica geplaatst en wordt "lui" aangeroepen voor elk exemplaar van het bedrijfsproces op het moment dat het uit de database wordt hersteld;
  • Als u de status van alle procesinstanties snel en synchroon moet migreren, worden de meer klassieke oplossingen voor databasemigratie gebruikt. Hierbij moet u echter met JSON werken.

Hebben we een ander bedrijfsproceskader nodig?

Dankzij de in het artikel beschreven oplossingen konden we ons leven aanzienlijk vereenvoudigen, het scala aan problemen dat op het niveau van toegepaste ontwikkeling werd opgelost, uitbreiden en het idee om bedrijfslogica op te splitsen in microservices aantrekkelijker maken. Er is veel werk verricht om dit te bereiken; er is een zeer "licht" raamwerk voor bedrijfsprocessen gecreëerd, evenals servicecomponenten voor het oplossen van de geïdentificeerde problemen in de context van een breed scala aan toegepaste taken. We willen deze resultaten delen en de ontwikkeling van gemeenschappelijke componenten open source maken onder een vrije licentie. Dit zal enige moeite en tijd kosten. Als we begrijpen dat er behoefte is aan zulke oplossingen, kan dat voor ons een extra stimulans zijn. In het voorgestelde artikel wordt weinig aandacht besteed aan de mogelijkheden van het framework zelf, maar een aantal daarvan zijn wel zichtbaar in de gegeven voorbeelden. Mochten we ons raamwerk publiceren, dan bespreken we dat in een apart artikel. In de tussentijd stellen wij het op prijs als u een kleine feedback achterlaat door de volgende vraag te beantwoorden:

Alleen geregistreerde gebruikers kunnen deelnemen aan het onderzoek. Inloggen, Alsjeblieft.

Hebben we een ander bedrijfsproceskader nodig?

  • 18,8%ja, we zijn al heel lang op zoek naar zoiets3

  • 12,5%Ik ben geïnteresseerd om meer te weten te komen over uw implementatie, het kan nuttig zijn2

  • 6,2%we gebruiken een van de bestaande frameworks, maar we denken erover om deze te vervangen1

  • 18,8%we gebruiken een van de bestaande frameworks, alles is prima3

  • 18,8%we redden het zonder raamwerk3

  • 25,0%schrijf je eigen4

16 gebruikers hebben gestemd. 7 gebruikers onthielden zich van stemming.

Bron: www.habr.com

Koop betrouwbare hosting voor sites met DDoS-bescherming, VPS VDS-servers 🔥 Koop betrouwbare websitehosting met DDoS-bescherming, VPS- en VDS-servers | ProHoster