Integratie in BPM-stijl

Integratie in BPM-stijl

Hello, Habr!

Ons bedrijf is gespecialiseerd in de ontwikkeling van softwareoplossingen van ERP-klasse, waarvan het leeuwendeel wordt ingenomen door transactiesystemen met een enorme hoeveelheid bedrijfslogica en documentstroom à la EDMS. De huidige versies van onze producten zijn gebaseerd op JavaEE-technologieën, maar we experimenteren ook actief met microservices. Een van de meest problematische gebieden van dergelijke oplossingen is de integratie van verschillende subsystemen die tot aangrenzende domeinen behoren. Integratieproblemen hebben ons altijd enorme hoofdpijn bezorgd, ongeacht de architecturale stijlen, technologiepakketten en raamwerken die we gebruiken, maar de laatste tijd is er vooruitgang geboekt bij het oplossen van dergelijke problemen.

In het artikel dat ik onder uw aandacht breng, zal ik vertellen over de ervaring en het architectuuronderzoek dat NPO “Krista” heeft in het aangewezen gebied. We zullen ook kijken naar een voorbeeld van een eenvoudige oplossing voor een integratieprobleem vanuit het standpunt van een applicatieontwikkelaar en ontdekken wat er achter deze eenvoud schuilgaat.

Disclaimer

De architectonische en technische oplossingen beschreven in het artikel worden door mij voorgesteld op basis van persoonlijke ervaring in de context van specifieke taken. Deze oplossingen beweren 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 kenmerken van de toegepaste problemen van onze oplossingen. Het grootste deel van de bedrijfslogica in ons typische transactiesysteem bestaat uit het invoeren van gegevens in de database via gebruikersinterfaces, handmatige en geautomatiseerde verificatie van deze gegevens, het uitvoeren ervan via een bepaalde workflow, het publiceren ervan naar een ander systeem/analytische database/archief, het genereren van rapporten . De belangrijkste functie van het systeem voor klanten is dus de automatisering van hun interne bedrijfsprocessen.

Gemakshalve gebruiken we de term ‘document’ in communicatie als een abstractie van een reeks gegevens verenigd door een gemeenschappelijke sleutel waaraan een bepaalde workflow kan worden ‘gekoppeld’.
Maar hoe zit het met de integratielogica? De integratietaak wordt immers gegenereerd door de architectuur van het systeem, die NIET op verzoek van de klant in delen wordt ‘geknipt’, maar onder invloed van totaal verschillende factoren:

  • onderworpen aan de wet van Conway;
  • als gevolg van het hergebruik van subsystemen die eerder voor andere producten zijn ontwikkeld;
  • naar keuze van de architect, op basis van niet-functionele eisen.

De verleiding is groot om de integratielogica te scheiden van de bedrijfslogica van de hoofdworkflow, om de bedrijfslogica niet te vervuilen met integratieartefacten en de applicatieontwikkelaar te behoeden voor de noodzaak om zich te verdiepen in de kenmerken van het architecturale landschap van het systeem. Deze aanpak heeft een aantal voordelen, maar de praktijk toont de ineffectiviteit ervan:

  • het oplossen van integratieproblemen valt meestal terug op de eenvoudigste opties in de vorm van synchrone oproepen vanwege de beperkte uitbreidingspunten in de implementatie van de hoofdworkflow (de nadelen van synchrone integratie worden hieronder besproken);
  • integratieartefacten dringen nog steeds door in de kern van de bedrijfslogica wanneer feedback van een ander subsysteem vereist is;
  • de applicatieontwikkelaar negeert de integratie en kan deze gemakkelijk verbreken door de workflow te veranderen;
  • vanuit het gezichtspunt van de gebruiker is het systeem niet langer één geheel, worden ‘naden’ tussen subsystemen merkbaar en verschijnen er redundante gebruikershandelingen, waardoor de overdracht van gegevens van het ene subsysteem naar het andere wordt geïnitieerd.

Een andere benadering is om integratie-interacties te beschouwen als een integraal onderdeel van de kernbedrijfslogica en workflow. Om te voorkomen dat de kwalificaties van applicatieontwikkelaars omhoogschieten, moet het creëren van nieuwe integratie-interacties eenvoudig en moeiteloos zijn, met minimale mogelijkheden om een ​​oplossing te kiezen. Dit is moeilijker te doen dan het lijkt: het hulpmiddel moet krachtig genoeg zijn om de gebruiker de vereiste verscheidenheid aan gebruiksmogelijkheden te bieden, zonder dat hij zichzelf ‘in de voet schiet’. Er zijn veel vragen die een ingenieur moet beantwoorden in de context van integratietaken, maar waar een applicatieontwikkelaar in zijn dagelijkse werk niet aan moet denken: transactiegrenzen, consistentie, atomiciteit, veiligheid, schaalvergroting, verdeling van belasting en middelen, routering, marshaling, distributie en schakelcontexten, enz. Het is noodzakelijk om applicatieontwikkelaars vrij eenvoudige oplossingssjablonen aan te bieden waarin de antwoorden op al dergelijke vragen al verborgen zijn. Deze sjablonen moeten redelijk veilig zijn: de bedrijfslogica verandert heel vaak, wat het risico op het introduceren van fouten vergroot, de kosten van fouten moeten op een vrij laag niveau blijven.

Maar wat heeft BPM ermee 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 statustransitiediagram en de verbinding van handlers met bedrijfslogica voor transities. In dit geval is de toestand die de huidige positie van het “document” in het bedrijfsproces bepaalt een attribuut van het “document” zelf.

Integratie in BPM-stijl
Zo ziet het proces er bij de start van een project uit

De populariteit van deze implementatie is te danken aan de relatieve eenvoud en snelheid waarmee lineaire bedrijfsprocessen kunnen worden gecreëerd. Naarmate softwaresystemen echter voortdurend complexer worden, groeit en wordt het geautomatiseerde deel van het bedrijfsproces complexer. Er is behoefte aan ontleding, hergebruik van delen van processen en vertakkende processen, zodat elke vertakking parallel wordt uitgevoerd. Onder dergelijke omstandigheden wordt het hulpmiddel onhandig en verliest het statusovergangsdiagram zijn informatie-inhoud (integratie-interacties worden helemaal niet weerspiegeld in het diagram).

Integratie in BPM-stijl
Dit is hoe het proces eruit ziet na verschillende iteraties van verduidelijking van de vereisten.

De uitweg uit deze situatie was de integratie van de motor jBPM in een aantal producten met de meest complexe bedrijfsprocessen. Op de korte termijn had deze oplossing enig succes: het werd mogelijk om complexe bedrijfsprocessen te implementeren met behoud van een redelijk informatief en relevant diagram in de notatie BPMN2.

Integratie in BPM-stijl
Een klein onderdeel van een complex bedrijfsproces

Op de lange termijn voldeed de oplossing niet aan de verwachtingen: de hoge arbeidsintensiteit van het creëren van bedrijfsprocessen met behulp van visuele hulpmiddelen maakte het niet mogelijk aanvaardbare productiviteitsindicatoren te bereiken, en de tool zelf werd een van de meest geliefde onder ontwikkelaars. Er waren ook klachten over de interne structuur van de motor, wat leidde tot het verschijnen van veel "patches" en "krukken".

Het belangrijkste positieve aspect van het gebruik van jBPM was het bewustzijn van de voor- en nadelen van het hebben van een eigen persistente status van een bedrijfsprocesinstantie. We zagen ook de mogelijkheid om een ​​procesbenadering te gebruiken om complexe integratieprotocollen tussen verschillende applicaties te implementeren met behulp van asynchrone interacties via signalen en berichten. De aanwezigheid van een persistente staat speelt hierbij een cruciale rol.

Op basis van het bovenstaande kunnen we concluderen: De procesbenadering in de BPM-stijl stelt ons in staat een breed scala aan taken op te lossen om steeds complexere bedrijfsprocessen te automatiseren, integratieactiviteiten harmonieus in deze processen in te passen en de mogelijkheid te behouden om het geïmplementeerde proces visueel weer te geven in een geschikte notatie.

Nadelen van synchrone oproepen als integratiepatroon

Synchrone integratie verwijst naar de eenvoudigste blokkerende oproep. Eén subsysteem fungeert als serverzijde en stelt de API beschikbaar met de vereiste methode. Een ander subsysteem fungeert als clientzijde en belt op het juiste moment en wacht op het resultaat. Afhankelijk van de systeemarchitectuur kunnen de client- en serverzijde zich in dezelfde applicatie en hetzelfde proces bevinden, of in verschillende. In het tweede geval moet u een RPC-implementatie toepassen en de parameters en het resultaat van de oproep samenbrengen.

Integratie in BPM-stijl

Dit integratiepatroon heeft een vrij groot aantal nadelen, maar wordt in de praktijk op grote schaal gebruikt vanwege zijn eenvoud. De snelheid van de implementatie boeit en dwingt je om het keer op keer te gebruiken ondanks dringende deadlines, waarbij de oplossing als technische schuld wordt geregistreerd. Maar het komt ook voor dat onervaren ontwikkelaars het onbewust gebruiken, zonder zich de negatieve gevolgen ervan te realiseren.

Naast de meest voor de hand liggende toename van de connectiviteit van subsystemen, zijn er ook minder voor de hand liggende problemen met ‘groeiende’ en ‘uitrekkende’ transacties. Als de bedrijfslogica enkele wijzigingen doorvoert, kunnen transacties niet worden vermeden, en blokkeren transacties op hun beurt bepaalde applicatiebronnen die door deze wijzigingen worden beïnvloed. Dat wil zeggen, totdat het ene subsysteem wacht op een reactie van het andere, zal het de transactie niet kunnen voltooien en de vergrendelingen kunnen verwijderen. Dit verhoogt het risico op verschillende effecten aanzienlijk:

  • Het reactievermogen van het systeem gaat verloren, gebruikers wachten lang op antwoorden op verzoeken;
  • de server reageert over het algemeen niet meer op gebruikersverzoeken vanwege een overvolle threadpool: de meerderheid van de threads is vergrendeld op een bron die wordt ingenomen door een transactie;
  • Er beginnen impasses te ontstaan: de waarschijnlijkheid dat deze zich voordoen, hangt sterk af van de duur van de transacties, de hoeveelheid bedrijfslogica en blokkeringen die bij de transactie betrokken zijn;
  • Er verschijnen transactietime-outfouten;
  • de server “faalt” met OutOfMemory als de taak het verwerken en wijzigen van grote hoeveelheden gegevens vereist, en de aanwezigheid van synchrone integraties maakt het erg moeilijk om de verwerking op te splitsen in “lichtere” transacties.

Vanuit architectonisch oogpunt leidt het gebruik van het blokkeren van oproepen tijdens de integratie tot een verlies van controle over de kwaliteit van individuele subsystemen: het is onmogelijk om de beoogde kwaliteitsindicatoren van het ene subsysteem te garanderen los van de kwaliteitsindicatoren van een ander subsysteem. 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 je aan beide kanten synchrone wijzigingen moet aanbrengen. Hoe kunnen we de transactionaliteit van deze veranderingen garanderen?

Als wijzigingen in afzonderlijke transacties worden aangebracht, moet u zorgen voor een betrouwbare afhandeling en compensatie van uitzonderingen, en dit elimineert volledig het belangrijkste voordeel van synchrone integraties: eenvoud.

We denken ook aan gedistribueerde transacties, maar we gebruiken ze niet in onze oplossingen: het is moeilijk om de betrouwbaarheid te garanderen.

"Saga" als oplossing voor het transactieprobleem

Met de groeiende populariteit van microservices neemt de vraag naar Saga-patroon.

Dit patroon lost de bovengenoemde problemen van langdurige transacties perfect op, en breidt ook de mogelijkheden uit om de toestand van het systeem te beheren vanuit de bedrijfslogica: compensatie na een mislukte transactie brengt het systeem mogelijk niet terug naar de oorspronkelijke staat, maar biedt een alternatieve route voor gegevensverwerking. Hierdoor kunt u ook voorkomen dat u succesvol voltooide gegevensverwerkingsstappen herhaalt wanneer u probeert het proces tot een “goed” einde te brengen.

Interessant genoeg is dit patroon in monolithische systemen ook relevant als het gaat om de integratie van losjes gekoppelde subsystemen en worden negatieve effecten waargenomen die worden veroorzaakt door langlopende transacties en de bijbehorende blokkering van hulpbronnen.

Met betrekking tot onze bedrijfsprocessen in de BPM-stijl blijkt het heel eenvoudig om “Sagen” te implementeren: individuele stappen van de “Saga” kunnen worden gespecificeerd als activiteiten binnen het bedrijfsproces, en de aanhoudende staat van het bedrijfsproces ook bepaalt de interne toestand van de “Saga”. Dat wil zeggen dat we geen extra coördinatiemechanisme nodig hebben. Het enige dat u nodig hebt, is een berichtenmakelaar die “minstens één keer” garanties als transport ondersteunt.

Maar deze oplossing heeft ook zijn eigen “prijs”:

  • de bedrijfslogica wordt complexer: er moet een compensatie worden uitgewerkt;
  • het zal nodig zijn om de volledige consistentie op te geven, wat vooral gevoelig kan zijn voor monolithische systemen;
  • De architectuur wordt iets ingewikkelder en er ontstaat een extra behoefte aan een berichtenmakelaar;
  • Er zullen aanvullende monitoring- en beheertools nodig zijn (hoewel dit over het algemeen goed is: de kwaliteit van de systeemservice zal toenemen).

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

Bedrijfslogica insluiten in microservices

Toen we begonnen te experimenteren met microservices rees er een redelijke vraag: waar moesten we de domeinbedrijfslogica plaatsen in relatie tot de service die de persistentie van domeingegevens garandeert?

Wanneer we naar de architectuur van verschillende BPMS’en kijken, lijkt het misschien redelijk om bedrijfslogica te scheiden van persistentie: creëer een laag van platform- en domeinonafhankelijke microservices die een omgeving en container vormen voor het uitvoeren van domeinbedrijfslogica, en ontwerp de persistentie van domeingegevens als een aparte laag van zeer eenvoudige en lichtgewicht microservices. Bedrijfsprocessen voeren in dit geval de orkestratie uit van de services van de persistentielaag.

Integratie in BPM-stijl

Deze aanpak heeft een heel groot voordeel: je kunt de functionaliteit van het platform zoveel vergroten als je wilt, en alleen de bijbehorende laag platformmicroservices wordt hierdoor ‘dik’. Bedrijfsprocessen uit welk domein dan ook kunnen direct gebruik maken van de nieuwe functionaliteit van het platform zodra deze geüpdatet is.

Een meer gedetailleerd onderzoek bracht aanzienlijke nadelen van deze aanpak aan het licht:

  • een platformdienst die de bedrijfslogica van veel domeinen tegelijk uitvoert, brengt grote risico's met zich mee als single point of Failure. Frequente wijzigingen in de bedrijfslogica vergroten het risico op fouten die tot systeembrede storingen leiden;
  • prestatieproblemen: bedrijfslogica werkt met zijn gegevens via een smalle en trage interface:
    • de gegevens zullen opnieuw worden verzameld en door de netwerkstack worden gepompt;
    • een domeinservice zal vaak meer gegevens leveren dan nodig is om de bedrijfslogica te verwerken vanwege onvoldoende mogelijkheden voor het parametriseren van verzoeken op het niveau van de externe API van de service;
    • verschillende onafhankelijke delen van de bedrijfslogica kunnen herhaaldelijk dezelfde gegevens opnieuw aanvragen voor verwerking (dit probleem kan worden verholpen door sessiecomponenten toe te voegen die gegevens in de cache opslaan, maar dit compliceert de architectuur verder en creëert problemen met de relevantie van gegevens en het ongeldig maken van de cache);
  • transactieproblemen:
    • bedrijfsprocessen met een persistente status, die zijn opgeslagen door een platformservice, zijn inconsistent met domeingegevens, en er zijn geen gemakkelijke manieren om dit probleem op te lossen;
    • het plaatsen van domeingegevensblokkering buiten de transactie: als de domeinbedrijfslogica wijzigingen moet aanbrengen nadat eerst de juistheid van de huidige gegevens is gecontroleerd, is het noodzakelijk om de mogelijkheid van een concurrentieverandering 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 problemen bij het updaten: in sommige gevallen moeten de persistentieservice en de bedrijfslogica synchroon of in strikte volgorde worden bijgewerkt.

Uiteindelijk moesten we terug naar de basis: domeingegevens en domeinbedrijfslogica in één microservice inkapselen. Deze aanpak vereenvoudigt de perceptie van een microservice als een integraal onderdeel van het systeem en geeft geen aanleiding tot de bovengenoemde problemen. Dit wordt ook niet gratis gegeven:

  • API-standaardisatie is vereist voor interactie met bedrijfslogica (in het bijzonder om gebruikersactiviteiten aan te bieden als onderdeel van bedrijfsprocessen) en API-platformdiensten; vereist meer zorgvuldige aandacht voor API-wijzigingen, voorwaartse en achterwaartse compatibiliteit;
  • het is noodzakelijk om extra runtimebibliotheken toe te voegen om de werking van bedrijfslogica als onderdeel van elk van deze microservices te garanderen, en dit leidt tot nieuwe vereisten voor dergelijke bibliotheken: lichtheid en een minimum aan transitieve afhankelijkheden;
  • Ontwikkelaars van bedrijfslogica moeten bibliotheekversies monitoren: als een microservice lange tijd niet is afgerond, zal deze hoogstwaarschijnlijk een verouderde versie van de bibliotheken bevatten. Dit kan een onverwacht obstakel zijn bij het toevoegen van een nieuwe functie en het kan nodig zijn om de oude bedrijfslogica van een dergelijke service naar nieuwe versies van bibliotheken te migreren als er incompatibele wijzigingen tussen versies zijn.

Integratie in BPM-stijl

In een dergelijke architectuur is ook een laag platformdiensten aanwezig, maar deze laag vormt niet langer een container voor het uitvoeren van domeinbedrijfslogica, maar alleen de omgeving ervan, die aanvullende “platform”-functies biedt. 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 van alle domeinen in de algemene lijst zien, wat betekent dat er een overeenkomstige platformtaakregistratieservice moet zijn, vrij van domeinbedrijfslogica. Het handhaven van de inkapseling van bedrijfslogica in een dergelijke context is behoorlijk problematisch, en dit is weer een compromis van deze architectuur.

Integratie van bedrijfsprocessen door de ogen van een applicatieontwikkelaar

Zoals hierboven vermeld, moet een applicatieontwikkelaar worden geabstraheerd van de technische en technische kenmerken van het implementeren van de interactie tussen verschillende applicaties, zodat men kan rekenen op een goede ontwikkelingsproductiviteit.

Laten we proberen een nogal moeilijk integratieprobleem op te lossen, speciaal bedacht voor het artikel. Dit zal een “game”-taak zijn waarbij drie applicaties betrokken zijn, waarbij elk van hen een bepaalde domeinnaam definieert: “app1”, “app2”, “app3”.

Binnen elke applicatie worden bedrijfsprocessen gelanceerd die via de integratiebus een balletje gaan slaan. Berichten met de naam “Bal” fungeren als een 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 verklaren hun deelname aan het spel, ‘leren elkaar en de startspeler kennen’;
  • na ontvangst van de bal selecteert de speler een andere deelnemende speler en geeft de bal naar hem door. Het totale aantal transmissies wordt geteld;
  • Elke speler heeft "energie" die afneemt bij elke balpass van die speler. Wanneer de energie op is, verlaat de speler het spel en kondigt zijn ontslag aan;
  • als de speler met rust wordt gelaten, kondigt hij onmiddellijk zijn vertrek aan;
  • Wanneer alle spelers zijn geëlimineerd, verklaart de eerste speler het spel voorbij. Als hij het spel voortijdig verlaat, blijft hij het spel volgen om het te voltooien.

Om dit probleem op te lossen, zal ik onze DSL voor bedrijfsprocessen gebruiken, waardoor we de logica in Kotlin compact kunnen beschrijven, met een minimum aan standaardteksten.

Het bedrijfsproces van de eerste speler (ook wel de initiatiefnemer van het spel genoemd) werkt in de app1-applicatie:

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 bovenstaande code een objectmodel van een bedrijfsproces opleveren, dat in de vorm van een diagram kan worden gevisualiseerd. We hebben de visualisator nog niet geïmplementeerd, dus we moesten wat tijd besteden aan tekenen (hier heb ik de BPMN-notatie enigszins vereenvoudigd met betrekking tot het gebruik van poorten om de consistentie van het diagram met de onderstaande code te verbeteren):

Integratie in BPM-stijl

app2 omvat het bedrijfsproces van de andere speler:

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:

Integratie in BPM-stijl

In de app3-applicatie zullen we een speler maken 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}")
}

Anders verschilt het gedrag van de speler niet van het 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 geven, om het artikel niet te vervuilen met een standaardtekst (in feite heb ik de eerder gemaakte testomgeving gebruikt om de integratie van andere bedrijfsprocessen te testen):

testspel()

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

Laten we de test uitvoeren en het logboek bekijken:

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 we een aantal belangrijke conclusies trekken:

  • met de nodige tools kunnen applicatieontwikkelaars integratie-interacties tussen applicaties creëren zonder de bedrijfslogica te onderbreken;
  • de complexiteit van een integratietaak die technische competenties vereist, kan binnen het raamwerk worden verborgen als dit in eerste instantie in de architectuur van het raamwerk wordt opgenomen. De moeilijkheid van een probleem kan niet worden verborgen, dus de oplossing voor een moeilijk probleem in code zal er zo uitzien;
  • Bij het ontwikkelen van integratielogica is het absoluut noodzakelijk om rekening te houden met de uiteindelijke consistentie en het gebrek aan lineariseerbaarheid van veranderingen in de toestand van alle integratiedeelnemers. Dit dwingt ons de logica ingewikkelder te maken, zodat deze ongevoelig wordt voor de volgorde waarin externe gebeurtenissen plaatsvinden. In ons voorbeeld wordt de speler gedwongen deel te nemen aan het spel nadat hij zijn vertrek uit het spel heeft aangegeven: andere spelers zullen de bal naar hem blijven doorgeven totdat de informatie over zijn vertrek alle deelnemers bereikt en verwerkt heeft. Deze logica volgt niet uit de spelregels en is een compromisoplossing binnen het raamwerk van de gekozen architectuur.

Vervolgens zullen we het hebben over de verschillende complexiteiten van onze oplossing, 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 voor berichten en één BPMTopic topic voor signalen (events). Alle berichten in één wachtrij plaatsen is op zichzelf al een compromis. Op het niveau van de bedrijfslogica kunt u nu zoveel nieuwe berichttypen introduceren als u wilt, zonder wijzigingen aan te brengen in de systeemstructuur. Dit is een aanzienlijke vereenvoudiging, maar brengt bepaalde risico's met zich mee, die ons in de context van onze typische taken niet zo belangrijk leken.

Integratie in BPM-stijl

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 u de “zichtbaarheidsscope” van het signaal wilt beperken tot één enkele applicatie. Dit zou de busdoorvoer moeten vergroten, maar de bedrijfslogica moet nu werken met domeinnamen: voor het adresseren van berichten - verplicht, voor signalen - wenselijk.

Zorgen voor de betrouwbaarheid van de integratiebus

Betrouwbaarheid bestaat uit verschillende punten:

  • De geselecteerde message broker is een cruciaal onderdeel van de architectuur en een single point of fail: hij moet voldoende fouttolerant zijn. U moet alleen beproefde implementaties gebruiken, met goede ondersteuning en een grote community;
  • het is noodzakelijk om een ​​hoge beschikbaarheid van de message broker te garanderen, waarvoor deze fysiek gescheiden moet zijn van de geïntegreerde applicaties (hoge beschikbaarheid van applicaties met toegepaste bedrijfslogica is veel moeilijker en duurder om te garanderen);
  • de makelaar is verplicht “minstens één keer” leveringsgaranties af te geven. Dit is een verplichte vereiste voor een betrouwbare werking van de integratiebus. Er is geen behoefte aan garanties op ‘precies één keer’ niveau: bedrijfsprocessen zijn in de regel niet gevoelig voor de herhaalde binnenkomst van berichten of gebeurtenissen, en bij speciale taken waarbij dit belangrijk is, is het gemakkelijker om extra controles aan de bedrijfsvoering toe te voegen logica dan voortdurend gebruik te maken van vrij “dure” garanties;
  • het verzenden van berichten en signalen moet betrokken zijn bij een algehele transactie met veranderingen in de staat van bedrijfsprocessen en domeingegevens. De voorkeur gaat uit naar het gebruik van een patroon Transactionele outbox, maar hiervoor zijn een extra tabel in de database en een repeater nodig. In JEE-applicaties kan dit worden vereenvoudigd door gebruik te maken van een lokale JTA-manager, maar de verbinding met de geselecteerde makelaar moet kunnen werken in XA;
  • afhandelaars van inkomende berichten en gebeurtenissen moeten ook werken met een transactie die de status van een bedrijfsproces verandert: als zo’n transactie wordt teruggedraaid, moet de ontvangst van het bericht worden geannuleerd;
  • berichten die vanwege fouten niet konden worden afgeleverd, moeten in een aparte opslag worden opgeslagen D.L.Q. (wachtrij met dode letters). Voor dit doel hebben we een afzonderlijke platformmicroservice gemaakt die dergelijke berichten in de opslag opslaat, ze indexeert op kenmerken (voor snel groeperen en zoeken) en een API beschikbaar stelt voor het bekijken, opnieuw verzenden naar het bestemmingsadres en het verwijderen van berichten. Systeembeheerders kunnen via hun webinterface met deze service werken;
  • in de brokerinstellingen moet u het aantal nieuwe bezorgpogingen en vertragingen tussen bezorgingen aanpassen om de kans te verkleinen dat berichten in DLQ terechtkomen (het is bijna onmogelijk om de optimale parameters te berekenen, maar u kunt empirisch handelen en deze tijdens de werking aanpassen );
  • De DLQ-winkel moet continu worden gemonitord en het monitoringsysteem moet systeembeheerders waarschuwen, zodat ze zo snel mogelijk kunnen reageren als er niet-bezorgde berichten voorkomen. Dit zal het “getroffen gebied” van een storing of bedrijfslogische fout verkleinen;
  • de integratiebus moet ongevoelig zijn voor de tijdelijke afwezigheid van applicaties: abonnementen op een onderwerp moeten duurzaam zijn, en de domeinnaam van de applicatie moet uniek zijn, zodat terwijl de applicatie afwezig is, iemand anders niet zal proberen zijn berichten van de applicatie te verwerken wachtrij.

Garanderen van draadveiligheid van bedrijfslogica

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

De bedrijfslogica van een proces verwerkt elke externe gebeurtenis die dat bedrijfsproces afzonderlijk beïnvloedt. Dergelijke gebeurtenissen kunnen zijn:

  • het starten van een bedrijfsprocesinstantie;
  • gebruikersactie gerelateerd aan 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;
  • controleactie via API (bijvoorbeeld procesonderbreking).

Elke dergelijke gebeurtenis kan de status van een bedrijfsprocesinstantie veranderen: sommige activiteiten kunnen eindigen en andere kunnen beginnen, en de waarden van persistente eigenschappen kunnen veranderen. Het sluiten van een activiteit kan resulteren in de activering van een of meer van de volgende activiteiten. Die kunnen op hun beurt stoppen met wachten op andere gebeurtenissen of, als ze geen aanvullende gegevens nodig hebben, dezelfde transactie voltooien. Voordat de transactie wordt afgesloten, wordt de nieuwe status van het bedrijfsproces opgeslagen in de database, waar wordt gewacht op de volgende externe gebeurtenis.

Persistente bedrijfsprocesgegevens die zijn opgeslagen in een relationele database zijn een zeer handig punt voor het synchroniseren van de verwerking als u SELECT FOR UPDATE gebruikt. Als één transactie erin slaagt de status van een bedrijfsproces te verkrijgen van de basis om het te veranderen, dan zal geen enkele parallelle transactie dezelfde status kunnen verkrijgen voor een andere verandering, en na de voltooiing van de eerste transactie zal de tweede transactie plaatsvinden. gegarandeerd de reeds gewijzigde staat te ontvangen.

Met pessimistische sloten aan de DBMS-kant voldoen we aan alle noodzakelijke eisen ACID, en behoud ook de mogelijkheid om de applicatie te schalen met bedrijfslogica door het aantal actieve instances te vergroten.

Pessimistische blokkades bedreigen ons echter met impasses, wat betekent dat SELECTEER VOOR UPDATE nog steeds beperkt moet blijven tot een redelijke time-out voor het geval er impasses optreden in sommige flagrante gevallen in de bedrijfslogica.

Een ander probleem is de synchronisatie van de start van een bedrijfsproces. Hoewel er geen exemplaar van een bedrijfsproces is, is er geen status in de database, dus de beschreven methode zal niet werken. Als u de uniciteit van een bedrijfsprocesinstantie in een specifiek bereik wilt garanderen, hebt u een soort synchronisatieobject nodig dat is gekoppeld aan de procesklasse en het bijbehorende bereik. Om dit probleem op te lossen, gebruiken we een ander vergrendelingsmechanisme waarmee we een willekeurige bron kunnen vergrendelen die is gespecificeerd door een sleutel in URI-indeling via een externe service.

In onze voorbeelden bevat het bedrijfsproces van InitialPlayer een declaratie

uniqueConstraint = UniqueConstraints.singleton

Daarom bevat het logboek berichten over het nemen en vrijgeven van het slot van de bijbehorende sleutel. Dergelijke berichten bestaan ​​niet voor andere bedrijfsprocessen: uniqueConstraint is niet ingesteld.

Problemen van bedrijfsprocessen met een aanhoudende status

Soms helpt het hebben van een aanhoudende toestand niet alleen de ontwikkeling, maar belemmert deze ook echt.
Problemen beginnen wanneer er wijzigingen moeten worden aangebracht in de bedrijfslogica en/of het bedrijfsprocesmodel. Niet al deze veranderingen zijn verenigbaar met de oude staat van bedrijfsprocessen. Als er veel live-instances in de database zijn, kan het maken van incompatibele wijzigingen veel problemen veroorzaken, wat we vaak tegenkwamen bij het gebruik van jBPM.

Afhankelijk van de diepte van de veranderingen, kunt u op twee manieren handelen:

  1. maak een nieuw bedrijfsprocestype om geen incompatibele wijzigingen aan te brengen in het oude, en gebruik dit in plaats van het oude bij het starten van nieuwe exemplaren. Oude exemplaren blijven werken “zoals voorheen”;
  2. migreer de persistente status van bedrijfsprocessen bij het updaten van bedrijfslogica.

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

  • duplicatie van bedrijfslogica in veel bedrijfsprocesmodellen, waardoor het volume van bedrijfslogica toeneemt;
  • Vaak is een onmiddellijke transitie naar nieuwe bedrijfslogica vereist (in termen van integratietaken - bijna altijd);
  • de ontwikkelaar weet niet op welk punt verouderde modellen kunnen worden verwijderd.

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

  • In de database wordt de persistente status van een bedrijfsproces opgeslagen in een gemakkelijk leesbare en gemakkelijk te verwerken vorm: in een string van JSON-formaat. Hierdoor kunnen migraties zowel binnen de applicatie als extern worden uitgevoerd. Als laatste redmiddel kunt u dit handmatig corrigeren (vooral handig bij het ontwikkelen tijdens het debuggen);
  • de integratiebedrijfslogica maakt geen gebruik van de namen van bedrijfsprocessen, zodat het op elk moment mogelijk is om de implementatie van een van de deelnemende processen te vervangen door een nieuwe met een nieuwe naam (bijvoorbeeld “InitialPlayerV2”). De binding vindt plaats via bericht- en signaalnamen;
  • het procesmodel heeft een versienummer, dat we verhogen als we incompatibele wijzigingen in dit model aanbrengen, en dit nummer wordt samen met de status van de procesinstantie 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” genoemd voor elk exemplaar van het bedrijfsproces op het moment dat het uit de database wordt hersteld;
  • als je de status van alle procesinstanties snel en synchroon moet migreren, worden meer klassieke databasemigratieoplossingen gebruikt, maar moet je met JSON werken.

Heeft u een ander raamwerk nodig voor bedrijfsprocessen?

De oplossingen die in het artikel worden beschreven, hebben ons in staat gesteld ons leven aanzienlijk te vereenvoudigen, het aantal problemen dat op applicatieontwikkelingsniveau is opgelost uit te breiden en het idee om bedrijfslogica in microservices te scheiden aantrekkelijker te maken. Om dit te bereiken is er veel werk verzet, er is een zeer “lichtgewicht” raamwerk voor bedrijfsprocessen gecreëerd, evenals servicecomponenten om de geïdentificeerde problemen op te lossen in de context van een breed scala aan applicatieproblemen. We willen deze resultaten delen en de ontwikkeling van gemeenschappelijke componenten open access maken onder een gratis licentie. Dit zal enige inspanning en tijd vergen. Het begrijpen van de vraag naar dergelijke oplossingen zou voor ons een extra stimulans kunnen zijn. In het voorgestelde artikel wordt zeer weinig aandacht besteed aan de mogelijkheden van het raamwerk zelf, maar sommige daarvan zijn zichtbaar in de gepresenteerde voorbeelden. Als we ons raamwerk publiceren, zal er een apart artikel aan worden gewijd. In de tussentijd zouden we het op prijs stellen als u wat feedback achterlaat door de vraag te beantwoorden:

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

Heeft u een ander raamwerk nodig voor bedrijfsprocessen?

  • 18,8%Ja, ik zoek al heel lang naar zoiets

  • 12,5%Ik ben geïnteresseerd in meer informatie over uw implementatie. Het kan nuttig zijn2

  • 6,2%We gebruiken een van de bestaande raamwerken, maar denken erover om1 te vervangen

  • 18,8%We gebruiken een van de bestaande raamwerken, alles is in orde3

  • 18,8%wij besturen zonder kader3

  • 25,0%schrijf de jouwe4

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

Bron: www.habr.com

Voeg een reactie