Integratie in BPM-stijl

Integratie in BPM-stijl

Hallo, Habr!

Ons bedrijf is gespecialiseerd in de ontwikkeling van ERP-softwareoplossingen, waarvan een groot deel bestaat uit transactiesystemen met een enorme hoeveelheid bedrijfslogica en documentworkflow, vergelijkbaar met EDMS. De moderne 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 betrekking hebben op aangrenzende domeinen. Integratietaken hebben ons altijd veel hoofdpijn bezorgd, ongeacht de architecturale stijlen, technologische stacks en frameworks die we gebruikten, maar de laatste tijd is er vooruitgang geboekt bij het oplossen van deze taken.

In het artikel dat we u aanbieden, zal ik de ervaringen en architectonische overpeinzingen van NPO 'Krista' in dit gebied bespreken. We zullen ook een voorbeeld bekijken van een eenvoudige oplossing voor een integratieprobleem vanuit het perspectief van een applicatieontwikkelaar en ontdekken wat er achter deze eenvoud schuilt.

Disclaimer

De in het artikel beschreven architectonische en technische oplossingen worden door mij voorgesteld op basis van persoonlijke ervaring in de context van specifieke taken. Deze oplossingen claimen niet universeel te zijn en kunnen in andere gebruiksomstandigheden mogelijk niet optimaal zijn.

Wat heeft BPM hiermee te maken?

Om deze vraag te beantwoorden, moeten we iets dieper ingaan op de specificiteit van de toepassingen 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 controle van deze gegevens, het doorvoeren van deze gegevens via een bepaalde workflow, publicatie in een ander systeem / analytische database / archief, en het genereren van rapporten. Zo is de sleutelrol van het systeem voor de klanten het automatiseren van hun interne bedrijfsprocessen.

Voor de duidelijkheid gebruiken we in onze communicatie de term ā€˜document’ als een abstractie van een set gegevens die zijn samengebracht op basis van 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, dat in delen is ā€˜opgesplitst’ niet op verzoek van de klant, maar onder invloed van heel andere factoren:

  • onder invloed van de wet van Conway;
  • door hergebruik van subsystemen die eerder voor andere producten zijn ontwikkeld;
  • volgens de architectuur, op basis van niet-functionele vereisten.

Er is een grote verleiding om de integratielogica te scheiden van de bedrijfslogica van de belangrijkste workflow, om de bedrijfslogica niet te vervuilen met integratieartefacten en om de applicatieontwikkelaar te ontlasten van de noodzaak om zich in de specifieke architectonische opzet van het systeem te verdiepen. Deze benadering heeft verschillende voordelen, maar de praktijk toont de ineffectiviteit ervan aan:

  • oplossingen voor integratievragen vervallen meestal in de eenvoudigste varianten van synchrone oproepen vanwege de beperkte uitbreidingspunten in de implementatie van de belangrijke workflow (over de nadelen van synchrone integratie - iets lager);
  • integratieartefacten dringen toch door in de belangrijkste bedrijfslogica wanneer feedback uit een ander subsysteem vereist is;
  • de applicatieontwikkelaar negeert de integratie en kan deze gemakkelijk verbreken door de workflow te wijzigen;
  • het systeem verliest zijn samenhang vanuit het perspectief van de gebruiker, de "naden" tussen subsysteem worden zichtbaar, en er ontstaan overbodige gebruikershandelingen die de overdracht van gegevens van het ene subsysteem naar het andere inluiden.

Een andere benadering is het beschouwen van integratie-interacties als een essentieel onderdeel van de kernbusinesslogica en workflow. Om de kwalificatie-eisen voor applicatie-ontwikkelaars niet torenhoog te maken, moet het creƫren van nieuwe integratie-interacties eenvoudig en ongecompliceerd worden uitgevoerd, met minimale opties voor het kiezen van een oplossingsmethode. Dit is lastiger dan het lijkt: de tool moet krachtig genoeg zijn om de gebruiker een voldoende scala aan toepassingsmogelijkheden te bieden en tegelijkertijd te voorkomen dat hij zichzelf in de voet schiet. Er zijn veel vragen waar een ingenieur op moet antwoorden in de context van integratietaken, maar waar de applicatie-ontwikkelaar zich in zijn dagelijkse werk niet mee bezig moet houden: transactieranden, consistentie, atomiciteit, beveiliging, schaalbaarheid, loadbalancing, routering, marshalling, contextverspreiding en -overschakeling, enzovoort. Er moeten voldoende eenvoudige oplossingssjablonen worden aangeboden aan applicatie-ontwikkelaars, waarin de antwoorden op al deze vragen al zijn verborgen. Deze sjablonen moeten veilig zijn: businesslogica verandert zeer vaak, wat het risico op fouten vergroot, en de kosten van fouten moeten op een redelijk laag niveau blijven.

Maar wat heeft BPM daar eigenlijk mee te maken? Er zijn toch talloze manieren om workflow te implementeren…
Inderdaad, in onze oplossingen is er een andere populaire manier om bedrijfsprocessen te implementeren – door de declaratieve opbouw van toestandsdiagrammen en de koppeling van handlers met businesslogica aan de overgangen. In dit geval is de toestand die de huidige positie van het "document" in het bedrijfsproces definieert, een attribuut van het "document" zelf.

Integratie in BPM-stijl
Zo ziet het proces eruit aan het begin van het project.

De populariteit van deze implementatie is te danken aan de relatieve eenvoud en snelheid waarmee lineaire bedrijfsprocessen kunnen worden gecreƫerd. Echter, naarmate software systemen complexer worden, groeit en compliceert de geautomatiseerde deel van het bedrijfsproces. Er ontstaat behoefte aan decompostie, hergebruik van procesdelen, en vertakking van processen, zodat elke tak parallel kan worden uitgevoerd. Onder dergelijke omstandigheden wordt het hulpmiddel ongemakkelijk, en het toestandsdiagram verliest zijn informativiteit (integratie-interacties worden helemaal niet weergegeven in het diagram).

Integratie in BPM-stijl
Zo ziet het proces eruit na enkele iteraties van vereistenverfijning.

De oplossing voor deze situatie was de integratie van de motor jBPM in enkele producten met de meest complexe bedrijfsprocessen. Op korte termijn had deze oplossing succes: er werd de mogelijkheid gecreƫerd om complexe bedrijfsprocessen uit te voeren terwijl er een redelijk informatief en actueel diagram in de notatie kwam. BPMN2.

Integratie in BPM-stijl
Een klein deel van het complexe bedrijfsproces.

Op lange termijn heeft de oplossing niet aan de verwachtingen voldaan: de hoge arbeidsintensiteit van het creƫren van bedrijfsprocessen via visuele hulpmiddelen heeft het niet mogelijk gemaakt om aanvaardbare productiviteit te bereiken, en het hulpmiddel werd een van de minst favoriete onder ontwikkelaars. Er waren ook klachten over de interne structuur van de motor, wat leidde tot de komst van vele 'patches' en 'hacks'.

Het belangrijkste positieve aspect van het gebruik van jBPM was het besef van de voordelen en nadelen van het hebben van een eigen persistente status van een instantie van een bedrijfsproces. Ook zagen we de mogelijkheid om een procesbenadering toe te passen voor het implementeren van complexe integratieprotocollen tussen verschillende applicaties met behulp van asynchrone interacties via signalen en berichten. Het hebben van een persistente status speelt hierbij een cruciale rol.

Op basis van het bovenstaande kan worden geconcludeerd: de procesbenadering in BPM-stijl stelt ons in staat een breed scala aan taken voor de automatisering van steeds complexer wordende bedrijfsprocessen op te lossen, integratieactiviteiten harmonieus in deze processen in te passen en de mogelijkheid te behouden om het gerealiseerde proces visueel weer te geven in de daarvoor geschikte notatie.

Nadelen van synchron calls als integratiepatroon

Onder synchronisatie-integratie verstaan we de eenvoudigste blokkerende aanroep. EƩn subsysteem fungeert als de serverzijde en stelt een API met de benodigde methode ter beschikking. Het andere subsysteem fungeert als de klantzijde en roept op het juiste moment aan met de verwachting van een resultaat. Afhankelijk van de systeemarchitectuur kunnen de klant- en serverzijde zich in ƩƩn applicatie en proces bevinden of in verschillende. In het laatste geval is een bepaalde implementatie van RPC vereist en moet de marshalling van parameters en het resultaat van de aanroep worden gewaarborgd.

Integratie in BPM-stijl

Dit integratiepatroon heeft een behoorlijk aantal nadelen, maar het wordt in de praktijk vaak gebruikt vanwege zijn eenvoud. De snelheid van implementatie is aantrekkelijk en dwingt tot herhaaldelijk gebruik in situaties met 'brandjes', waarbij de oplossing als technische schuld wordt vastgelegd. Maar soms passen onervaren ontwikkelaars het onbewust toe, simpelweg zonder zich bewust te zijn van de negatieve gevolgen.

Naast de meest voor de hand liggende verhoging van de coupling tussen subsystemen zijn er ook minder evidente problemen met het 'uitrekken' en 'uitrekken' van transacties. Inderdaad, als de bedrijfslogica bepaalde wijzigingen aanbrengt, zijn transacties onvermijdelijk, en transacties blokkeren op hun beurt bepaalde bronnen van de applicatie die door deze wijzigingen worden aangetast. Dat betekent dat zolang ƩƩn subsysteem niet op een antwoord van een ander subsysteem wacht, het de transactie niet kan voltooien en de blokkades kan opheffen. Dit verhoogt aanzienlijk het risico op verschillende effecten:

  • de responsiviteit van het systeem gaat verloren, gebruikers wachten lang op antwoorden op verzoeken;
  • de server reageert helemaal niet meer op gebruikersverzoeken door een overvol threadpool: de meeste threads zijn 'vastgelopen' door het blokkeren van een bron die door de transactie wordt gebruikt;
  • er ontstaan deadlocks: de kans op hun ontstaan hangt sterk af van de duur van de transacties, het aantal betrokken bedrijfslogica en blokkades;
  • er treden time-out fouten op bij de transactie;
  • de server 'crasht' door OutOfMemory als de taak vereist dat grote hoeveelheden gegevens worden verwerkt en gewijzigd, terwijl het hebben van synchronous integraties het moeilijk maakt om de verwerking op te splitsen in meer 'lichte' transacties.

Vanuit architectonisch perspectief leidt het gebruik van blokkerende aanroepen bij integratie tot verlies van controle over de kwaliteit van afzonderlijke subsystemen: het is onmogelijk om de kwaliteitsdoelstellingen van ƩƩn subsysteem te waarborgen los van de kwaliteitsindicatoren van een ander subsysteem. Als de subsystemen door verschillende teams worden ontwikkeld, vormt dit een groot probleem.

Het wordt nog interessanter als de te integreren subsystemen zich in verschillende applicaties bevinden en er van beide kanten synchronisatie wijzigingen moeten worden aangebracht. Hoe waarborgen we de transactie-integriteit van deze wijzigingen?

Als wijzigingen in afzonderlijke transacties worden aangebracht, moet er betrouwbare foutafhandeling en compensatie worden gewaarborgd, wat het belangrijkste voordeel van synchrone integraties - eenvoud - volledig tenietdoet.

Ik denk ook aan gedistribueerde transacties, maar we gebruiken ze niet in onze oplossingen: de betrouwbaarheid is moeilijk te waarborgen.

ā€˜Saga’ als oplossing voor het transactieprobleem

Met de groeiende populariteit van microservices krijgt het steeds meer aandacht Saga Pattern.

Dit patroon lost de eerder genoemde problemen van langdurige transacties uitstekend op en breidt de mogelijkheden voor systeemstatusbeheer vanuit de bedrijfslogica uit: compensatie na een mislukte transactie kan de systeemstatus niet terugzetten naar de oorspronkelijke staat, maar kan alternatieve verwerkingsroutes voor gegevens bieden. Dit maakt het ook mogelijk om succesvol voltooide stappen van gegevensverwerking niet te herhalen bij herhaalde pogingen om het proces tot een ā€˜goede’ eindresultaat te brengen.

Interessant is dat dit patroon ook relevant is in monolithische systemen als het gaat om de integratie van zwak gekoppelde subsystemen en er negatieve effecten zijn door langdurige transacties en de bijbehorende resource-locks.

Toegepast op onze BPM-stijl bedrijfsprocessen blijkt het implementeren van 'Sagas' heel eenvoudig: afzonderlijke stappen van de 'Saga' kunnen worden gedefinieerd als activiteiten binnen het bedrijfsproces, en de persistente status van het bedrijfsproces bepaalt onder andere de interne status van de 'Saga'. Dit betekent dat we geen extra coƶrdinatiemechanisme nodig hebben. We hebben alleen een berichtenbroker nodig die 'at least once' garanties ondersteunt als transport.

Maar ook deze oplossing heeft zijn eigen ā€˜prijs’:

  • de bedrijfslogica wordt complexer: compensaties moeten worden verwerkt;
  • het zal nodig zijn om volledige consistentie op te geven, wat vooral gevoelig kan zijn voor monolithische systemen;
  • de architectuur wordt iets ingewikkelder, er is een extra behoefte aan een berichtenbroker;
  • er zijn extra middelen voor monitoring en beheer nodig (hoewel dit in het algemeen goed is: de kwaliteit van de service van het systeem zal verbeteren).

Voor monolithische systemen is de rechtvaardiging voor het gebruik van 'Saga' niet zo duidelijk. Voor microservices en andere SOA's, waar waarschijnlijk al een broker aanwezig is en volledige consistentie op de start van het project is opgeofferd, kan het voordeel van het gebruik van dit patroon de nadelen aanzienlijk overstijgen, vooral als er een handige API op het niveau van de bedrijfslogica beschikbaar is.

Incapculatie van bedrijfslogica in microservices

Toen we begonnen te experimenteren met microservices, ontstond de redelijke vraag: waar moeten we de domein bedrijfslogica plaatsen ten opzichte van de service die de persistentie van domeingegevens waarborgt?

Bij het bekijken van de architectuur van verschillende BPMS lijkt het redelijk om de bedrijfslogica van persistentie te scheiden: een laag van platform- en domeinonafhankelijke microservices creƫren die een omgeving en container voor de uitvoering van domein bedrijfslogica vormen, terwijl de persistentie van domeingegevens wordt afgehandeld door een aparte laag van zeer eenvoudige en lichte microservices. De bedrijfsprocessen orkestreren in dat geval de services van de persistentielaag.

Integratie in BPM-stijl

Dit benadering heeft een groot voordeel: je kunt de functionaliteit van het platform naar hartenlust uitbreiden, en alleen de bijbehorende laag van platform microservices zal hierdoor 'dikker' worden. Bedrijfsprocessen uit elk domein krijgen onmiddellijk toegang tot nieuwe functionaliteit van het platform zodra deze wordt bijgewerkt.

Diepgaand onderzoek heeft aanzienlijke tekortkomingen van deze benadering aan het licht gebracht:

  • de platformservice die de bedrijfslogica van meerdere domeinen uitvoert, brengt grote risico's met zich mee als enkelvoudig foutpunt. Frequent wijzigingen in de bedrijfslogica verhogen het risico op fouten die leiden tot storingen die door het hele systeem verspreiden;
  • prestatieproblemen: de bedrijfslogica werkt met zijn eigen gegevens via een smalle en trage interface:
    • Gegevens worden herhaaldelijk gemarshaled en door de netstack verwerkt;
    • De domeindienst levert vaak meer gegevens dan de zakelijke logica nodig heeft voor verwerking, vanwege onvoldoende mogelijkheden voor parameterisatie van verzoeken op het niveau van de externe API-dienst;
    • Meerdere onafhankelijke delen van de zakelijke logica kunnen dezelfde gegevens opnieuw aanvragen voor verwerking (dit probleem kan worden verzacht door sessiecomponenten toe te voegen die gegevens cachen, maar dit compliceert de architectuur verder en creĆ«ert problemen met de actualiteit van de gegevens en het ongeldig maken van de cache);
  • Transactieproblemen:
    • Zakelijke processen met een persistent state, waarvan het opslaan door de platformdienst gebeurt, kunnen niet meer overeenkomen met de domeingegevens, en er zijn geen eenvoudige oplossingen voor dit probleem te voorzien;
    • Het verplaatsen van de blokkering van domeingegevens buiten de transactie: als de domein zakelijke logica wijzigingen moet aanbrengen, dient eerst de juistheid van de actuele gegevens te worden gecontroleerd, waarbij de mogelijkheid van gelijktijdige wijzigingen van de verwerkte gegevens moet worden uitgesloten. Externe gegevensblokkering kan helpen om dit probleem op te lossen, maar deze oplossing brengt extra risico's met zich mee en verlaagt de algehele betrouwbaarheid van het systeem;
  • Extra complicaties bij updates: in sommige gevallen moeten de persistentiedienst en de zakelijke logica synchroon of in een strikte volgorde worden bijgewerkt.

Uiteindelijk moesten we terug naar de basis: domeingegevens en domein zakelijke logica in ƩƩn microservice encapsuleren. Deze aanpak vereenvoudigt de perceptie van de microservice als een samenhangend component binnen het systeem en genereert de hierboven genoemde problemen niet. Dit kost echter ook iets:

  • Er is standaardisatie van de API nodig voor interactie met de zakelijke logica (in het bijzonder om gebruikersactiviteiten binnen zakelijke processen te waarborgen) en de API van de platformdiensten; er moet meer aandacht zijn voor veranderingen in de API, directe en achterwaartse compatibiliteit;
  • Er moeten extra runtime-bibliotheken worden toegevoegd om de werking van de zakelijke logica in elk van deze microservices te waarborgen, en dit genereert nieuwe vereisten aan deze bibliotheken: lichtgewichtheid en een minimum aan transitieve afhankelijkheden;
  • Ontwikkelaars van de bedrijfslogica moeten de versies van bibliotheken in de gaten houden: als een bepaalde microservice lange tijd niet wordt aangepast, is de kans groot dat deze een verouderde versie van bibliotheken bevat. Dit kan een onverwachte hindernis vormen voor het toevoegen van een nieuwe functie en kan vereisen dat de oude bedrijfslogica van die service wordt gemigreerd naar nieuwe versies van bibliotheken, als er incompatibele wijzigingen tussen de versies waren.

Integratie in BPM-stijl

In een dergelijke architectuur is er ook een laag van platformdiensten, maar deze laag vormt niet langer een container voor de uitvoering van de domeinbedrijfslogica, maar slechts de omgeving ervan, waarbij ondersteunende "platform"-functies worden geboden. Deze laag is nodig, niet alleen om de lichtgewichtheid van de domein-microservices te behouden, maar ook voor de centralisatie van het beheer.

Bijvoorbeeld, gebruikersactiviteiten in bedrijfsprocessen genereren taken. Bij het werken met taken moet de gebruiker echter taken uit alle domeinen in een algemene lijst kunnen zien, wat betekent dat er een overeenkomstige platformdienst voor het registreren van taken moet zijn, vrij van domeinbedrijfslogica. Het is vrij problematisch om de encapsulatie van bedrijfslogica in dergelijke context te behouden, en dit is opnieuw een compromis van deze architectuur.

Integratie van bedrijfsprocessen vanuit het perspectief van de applicatieontwikkelaar

Zoals eerder vermeld, moet de applicatieontwikkelaar worden geabstraheerd van de technische en engineeringaspecten van de implementatie van de interactie tussen verschillende applicaties, zodat een goede productiviteit van de ontwikkeling kan worden verwacht.

Laten we proberen een vrij complexe integratietaak op te lossen, speciaal bedacht voor dit artikel. Dit wordt een ā€˜speelse’ taak met deelname van drie applicaties, waarbij elke applicatie een bepaald domeinnaam definieert: ā€˜app1’, ā€˜app2’, ā€˜app3’.

Binnen elk applicatie worden bedrijfsprocessen gestart, die beginnen te ā€˜spelen met de bal’ via de integratiebus. In de rol van de bal zullen berichten met de naam ā€˜Ball’ optreden.

Spelregels:

  • de eerste speler is de initiator. Hij nodigt andere spelers uit om mee te doen, start het spel en kan het op elk moment beĆ«indigen;
  • andere spelers geven aan deel te nemen aan het spel, ā€˜leren elkaar’ kennen en leren de eerste speler kennen;
  • na het ontvangen van de bal kiest de speler een andere deelnemende speler en geeft de bal aan hem door. Het totale aantal passes wordt bijgehouden;
  • Elke speler heeft "energie" die afneemt bij elke pass die deze speler maakt. Wanneer de energie op is, verlaat de speler het spel en meldt zijn vertrek;
  • als de speler alleen is, meldt hij onmiddellijk zijn vertrek;
  • wanneer alle spelers zijn afgevallen, meldt de eerste speler het einde van het spel. Als hij eerder uit het spel is afgevallen, blijft hij het spel volgen om het te beĆ«indigen.

Voor deze taak gebruik ik onze DSL voor bedrijfsprocessen, die het mogelijk maakt om de logica compact in Kotlin te beschrijven, met minimaal boilerplate.

In de applicatie app1 zal het bedrijfsproces van de eerste speler (de initiator van het spel) draaien:

class 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()

// Dit is de klasse van het procesexemplaar: encapsuleert zijn interne toestand
class InitialPlayer : ProcessImpl(initialPlayerModel) {
    var playerName: String by persistent("Player1")
    var energy: Int by persistent(30)
    var players: PlayersList by persistent(PlayersList())
    var shotCounter: Int = 0
}

// Dit is de declaratie van het procesmodel: wordt een keer aangemaakt, gebruikt door alle
// exemplaren van de overeenkomstige klasse
val initialPlayerModel = processModel(name = "InitialPlayer",
                                                     version = 1) {

    // Volgens de regels is de eerste speler de initiator van het spel en moet uniek zijn
    uniqueConstraint = UniqueConstraints.singleton

    // We declareren de activiteiten waaruit het bedrijfsproces bestaat
    val sendNewGameSignal = signal("NewGame")
    val sendStopGameSignal = signal("StopGame")
    val startTask = humanTask("Start") {
        taskOperation {
            processCondition { players.size > 0 }
            confirmation { "${players.size} spelers zijn verbonden. Beginnen we?" }
        }
    }
    val stopTask = humanTask("Stop") {
        taskOperation {}
    }
    val waitPlayerJoin = signalWait("PlayerJoin") { signal ->
        players.add(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... speler ${signal.data} voegt zich toe ...")
    }
    val waitPlayerOut = signalWait("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... speler ${signal.data} is eruit ...")
    }
    val sendPlayerOut = signal("PlayerOut") {
        signalData = { playerName }
    }
    val sendHandshake = messageSend("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val throwStartBall = messageSend("Ball") {
        messageData = { 1 }
        activation = { selectNextPlayer() }
    }
    val throwBall = messageSend("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData("Ball") {
        shotCounter = it
    }

    // Nu construeren we het procesgrafiek van de gedeclareerde activiteiten
    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)
    }

    // We hangen extra handvatten aan de activiteiten voor logging
    sendNewGameSignal.onExit { println("Laten we spelen!") }
    sendStopGameSignal.onExit { println("Stop!") }
    sendPlayerOut.onExit { println("$playerName: Ik ben eruit!") }
}

private fun MessageSendInstance.selectNextPlayer() {
    val player = process.players.random()
    receiverDomain = player.domain
    receiverProcessInstanceId = player.id
    println("Stap ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

Naast de uitvoering van de zakelijke logica kan de gegeven code een objectmodel van het bedrijfsproces genereren, dat gevisualiseerd kan worden in de vorm van een diagram. We hebben de visualisator nog niet gerealiseerd, dus ik heb wat tijd besteed aan het tekenen (hier heb ik de BPMN-notatie iets vereenvoudigd in het gebruik van gateways om de consistentie van het diagram met de gegeven code te verbeteren):

Integratie in BPM-stijl

De applicatie app2 zal een bedrijfsproces van een andere speler omvatten:

class RandomPlayer

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

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

class PlayersList: ArrayList()

class RandomPlayer : ProcessImpl(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(name = "RandomPlayer", 
                                                   version = 1) {

    val waitNewGameSignal = signalWait("NewGame")
    val waitStopGameSignal = signalWait("StopGame")
    val sendPlayerJoin = signal("PlayerJoin") {
        signalData = { playerName }
    }
    val sendPlayerOut = signal("PlayerOut") {
        signalData = { playerName }
    }
    val waitPlayerJoin = signalWaitCustom("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("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        allPlayersOut = players.isEmpty()
    }
    val sendHandshake = messageSend("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val receiveHandshake = messageWait("Handshake") { message ->
        if (!players.any { message.sender.processInstanceId == it.id}) {
            players.add(PlayerInfo(
                    message.data!!, 
                    message.sender.domain, 
                    message.sender.processInstanceId))
        }
    }
    val throwBall = messageSend("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData("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: Ik ben hier!") }
    sendPlayerOut.onExit { println("$playerName: Ik ben eruit!") }
}

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

Diagram:

Integratie in BPM-stijl

In de app app3 zullen we de speler iets anders laten gedragen: in plaats van willekeurig de volgende speler te kiezen, zal hij volgens het round-robin-algoritme werken:

class 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()

class RoundRobinPlayer : ProcessImpl(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(
        name = "RoundRobinPlayer", 
        version = 1) {

    val waitNewGameSignal = signalWait("NewGame")
    val waitStopGameSignal = signalWait("StopGame")
    val sendPlayerJoin = signal("PlayerJoin") {
        signalData = { playerName }
    }
    val sendPlayerOut = signal("PlayerOut") {
        signalData = { playerName }
    }
    val waitPlayerJoin = signalWaitCustom("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("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!, 
                signal.sender.domain, 
                signal.sender.processInstanceId))
        allPlayersOut = players.isEmpty()
    }
    val sendHandshake = messageSend("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val receiveHandshake = messageWait("Handshake") { message ->
        if (!players.any { message.sender.processInstanceId == it.id}) {
            players.add(PlayerInfo(
                    message.data!!, 
                    message.sender.domain, 
                    message.sender.processInstanceId))
        }
    }
    val throwBall = messageSend("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData("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: Ik ben hier!") }
    sendPlayerOut.onExit { println("$playerName: Ik ben eruit!") }
}

private fun MessageSendInstance.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("Stap ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

Verder verandert het gedrag van de speler niet ten opzichte van het vorige, dus de diagram verandert niet.

Nu is er een test nodig om dit alles te laten draaien. Ik zal alleen de code van de test geven om de tekst niet te overladen met boilerplate (in werkelijkheid heb ik gebruik gemaakt van de testomgeving die eerder is gemaakt voor het testen van andere bedrijfsprocessen):

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");
    // Nu moeten we even wachten zodat de spelers elkaar "leren kennen".
    // Wachten met sleep is een slecht idee, maar het is wel het eenvoudigste. 
    // Doe dit niet in serieuze tests!
    Thread.sleep(1000);
    // We starten het spel en sluiten de gebruikersactiviteit af
    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 playerParams(String name, int energy) {
    Map params = new HashMap();
    params.put("playerName", name);
    params.put("energy", energy);
    return params;
}

We starten de test, kijken naar de log:

console output

De sleutel lock://app1/process/InitialPlayer is geblokkeerd.
Laten we spelen!
De sleutel lock://app1/process/InitialPlayer is gedeblokkeerd.
Speler2: Ik ben hier!
Speler3: Ik ben hier!
Speler4: Ik ben hier!
Speler5: Ik ben hier!
... speler Speler2 voegt zich toe ...
... speler Speler4 voegt zich toe ...
... speler Speler3 voegt zich toe ...
... speler Speler5 voegt zich toe ...
Stap 1: Speler1 >>> Speler3
Stap 2: Speler3 >>> Speler5
Stap 3: Speler5 >>> Speler3
Stap 4: Speler3 >>> Speler4
Stap 5: Speler4 >>> Speler3
Stap 6: Speler3 >>> Speler4
Stap 7: Speler4 >>> Speler5
Stap 8: Speler5 >>> Speler2
Stap 9: Speler2 >>> Speler5
Stap 10: Speler5 >>> Speler4
Stap 11: Speler4 >>> Speler2
Stap 12: Speler2 >>> Speler4
Stap 13: Speler4 >>> Speler1
Stap 14: Speler1 >>> Speler4
Stap 15: Speler4 >>> Speler3
Stap 16: Speler3 >>> Speler1
Stap 17: Speler1 >>> Speler2
Stap 18: Speler2 >>> Speler3
Stap 19: Speler3 >>> Speler1
Stap 20: Speler1 >>> Speler5
Stap 21: Speler5 >>> Speler1
Stap 22: Speler1 >>> Speler2
Stap 23: Speler2 >>> Speler4
Stap 24: Speler4 >>> Speler5
Stap 25: Speler5 >>> Speler3
Stap 26: Speler3 >>> Speler4
Stap 27: Speler4 >>> Speler2
Stap 28: Speler2 >>> Speler5
Stap 29: Speler5 >>> Speler2
Stap 30: Speler2 >>> Speler1
Stap 31: Speler1 >>> Speler3
Stap 32: Speler3 >>> Speler4
Stap 33: Speler4 >>> Speler1
Stap 34: Speler1 >>> Speler3
Stap 35: Speler3 >>> Speler4
Stap 36: Speler4 >>> Speler3
Stap 37: Speler3 >>> Speler2
Stap 38: Speler2 >>> Speler5
Stap 39: Speler5 >>> Speler4
Stap 40: Speler4 >>> Speler5
Stap 41: Speler5 >>> Speler1
Stap 42: Speler1 >>> Speler5
Stap 43: Speler5 >>> Speler3
Stap 44: Speler3 >>> Speler5
Stap 45: Speler5 >>> Speler2
Stap 46: Speler2 >>> Speler3
Stap 47: Speler3 >>> Speler2
Stap 48: Speler2 >>> Speler5
Stap 49: Speler5 >>> Speler4
Stap 50: Speler4 >>> Speler2
Stap 51: Speler2 >>> Speler5
Stap 52: Speler5 >>> Speler1
Stap 53: Speler1 >>> Speler5
Stap 54: Speler5 >>> Speler3
Stap 55: Speler3 >>> Speler5
Stap 56: Speler5 >>> Speler2
Stap 57: Speler2 >>> Speler1
Stap 58: Speler1 >>> Speler4
Stap 59: Speler4 >>> Speler1
Stap 60: Speler1 >>> Speler4
Stap 61: Speler4 >>> Speler3
Stap 62: Speler3 >>> Speler2
Stap 63: Speler2 >>> Speler5
Stap 64: Speler5 >>> Speler4
Stap 65: Speler4 >>> Speler5
Stap 66: Speler5 >>> Speler1
Stap 67: Speler1 >>> Speler5
Stap 68: Speler5 >>> Speler3
Stap 69: Speler3 >>> Speler4
Stap 70: Speler4 >>> Speler2
Stap 71: Speler2 >>> Speler5
Stap 72: Speler5 >>> Speler2
Stap 73: Speler2 >>> Speler1
Stap 74: Speler1 >>> Speler4
Stap 75: Speler4 >>> Speler1
Stap 76: Speler1 >>> Speler2
Stap 77: Speler2 >>> Speler5
Stap 78: Speler5 >>> Speler4
Stap 79: Speler4 >>> Speler3
Stap 80: Speler3 >>> Speler1
Stap 81: Speler1 >>> Speler5
Stap 82: Speler5 >>> Speler1
Stap 83: Speler1 >>> Speler4
Stap 84: Speler4 >>> Speler5
Stap 85: Speler5 >>> Speler3
Stap 86: Speler3 >>> Speler5
Stap 87: Speler5 >>> Speler2
Stap 88: Speler2 >>> Speler3
Speler2: Ik ben weg!
Stap 89: Speler3 >>> Speler4
... speler Speler2 is weg ...
Stap 90: Speler4 >>> Speler1
Stap 91: Speler1 >>> Speler3
Stap 92: Speler3 >>> Speler1
Stap 93: Speler1 >>> Speler4
Stap 94: Speler4 >>> Speler3
Stap 95: Speler3 >>> Speler5
Stap 96: Speler5 >>> Speler1
Stap 97: Speler1 >>> Speler5
Stap 98: Speler5 >>> Speler3
Stap 99: Speler3 >>> Speler5
Stap 100: Speler5 >>> Speler4
Stap 101: Speler4 >>> Speler5
Speler4: Ik ben weg!
... speler Speler4 is weg ...
Stap 102: Speler5 >>> Speler1
Stap 103: Speler1 >>> Speler3
Stap 104: Speler3 >>> Speler1
Stap 105: Speler1 >>> Speler3
Stap 106: Speler3 >>> Speler5
Stap 107: Speler5 >>> Speler3
Stap 108: Speler3 >>> Speler1
Stap 109: Speler1 >>> Speler3
Stap 110: Speler3 >>> Speler5
Stap 111: Speler5 >>> Speler1
Stap 112: Speler1 >>> Speler3
Stap 113: Speler3 >>> Speler5
Stap 114: Speler5 >>> Speler3
Stap 115: Speler3 >>> Speler1
Stap 116: Speler1 >>> Speler3
Stap 117: Speler3 >>> Speler5
Stap 118: Speler5 >>> Speler1
Stap 119: Speler1 >>> Speler3
Stap 120: Speler3 >>> Speler5
Stap 121: Speler5 >>> Speler3
Speler5: Ik ben weg!
... speler Speler5 is weg ...
Stap 122: Speler3 >>> Speler5
Stap 123: Speler5 >>> Speler1
Speler5: Ik ben weg!
Stap 124: Speler1 >>> Speler3
... speler Speler5 is weg ...
Stap 125: Speler3 >>> Speler1
Stap 126: Speler1 >>> Speler3
Speler1: Ik ben weg!
... speler Speler1 is weg ...
Stap 127: Speler3 >>> Speler3
Speler3: Ik ben weg!
Stap 128: Speler3 >>> Speler3
... speler Speler3 is weg ...
Speler3: Ik ben weg!
Stop!
Stap 129: Speler3 >>> Speler3
Speler3: Ik ben weg!

Uit dit alles kunnen enkele belangrijke conclusies worden getrokken:

  • met de juiste tools kunnen applicatie-ontwikkelaars integratie-interacties tussen applicaties creĆ«ren zonder de bedrijfslogica te verstoren;
  • de complexiteit van de integratietaak, die engineeringvaardigheden vereist, kan binnen het framework worden verborgen als dit vanaf de start in de architectuur is opgenomen. De moeilijkheid van de taak kan echter niet worden verborgen, daarom zal de oplossing van een moeilijke taak in de code dienovereenkomstig eruitzien;
  • bij het ontwikkelen van integratielogica moet rekening worden gehouden met eventual consistency en het ontbreken van lineariseerbaarheid van de toestandwijzigingen van alle betrokken integratiepartners. Dit dwingt ons de logica te compliceren om deze ongevoelig te maken voor de volgorde van externe gebeurtenissen. In ons voorbeeld is de speler gedwongen deel te nemen aan het spel nadat hij aangeeft uit het spel te stappen: andere spelers blijven de bal naar hem doorgeven totdat de informatie over zijn vertrek alle deelnemers heeft bereikt en is verwerkt. Deze logica komt niet voort uit de spelregels en is een compromisoplossing binnen de gekozen architectuur.

Laten we verder praten over de verschillende nuances van onze oplossing, compromissen en andere zaken.

Alle berichten – in ƩƩn wachtrij

Alle te integreren applicaties werken met één integratiebus, die wordt weergegeven als een externe broker, één BPMQueue voor berichten en één BPMTopic voor signalen (gebeurtenissen). Het doorlaten van alle berichten via één wachtrij is op zich al een compromis. Op het niveau van de bedrijfslogica kunnen nu onbeperkt nieuwe typen berichten worden geïntroduceerd, zonder wijzigingen aan de systeemstructuur aan te brengen. Dit is een aanzienlijke vereenvoudiging, maar het brengt bepaalde risico's met zich mee die in de context van onze typische taken voor ons niet zo significant leken.

Integratie in BPM-stijl

Er is echter ƩƩn nuance: elke toepassing filtert zijn eigen berichten al bij binnenkomst op basis van de naam van zijn domein. Ook kan het domein in de signalen worden aangegeven als de 'zichtbaarheid' van het signaal tot ƩƩn enkele toepassing moet worden beperkt. Dit zou de doorvoersnelheid van de bus moeten verhogen, maar de bedrijfslogica moet nu werken met domeinnamen: voor het adresseren van berichten is dit verplicht, voor signalen is dit wenselijk.

Zorg voor betrouwbaarheid van de integratiebus

Betrouwbaarheid bestaat uit verschillende factoren:

  • de gekozen berichtenbroker is een cruciaal onderdeel van de architectuur en een enkel punt van falen: hij moet voldoende fouttolerant zijn. Er moeten alleen beproefde implementaties worden gebruikt 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 toepassingen (het waarborgen van hoge beschikbaarheid van toepassingen met zakelijke logica is aanzienlijk complexer en duurder);
  • de broker moet 'at least once' leveringsgaranties bieden. Dit is een verplichting voor een betrouwbare werking van de integratiebus. Garantiestelsels op het niveau van 'exactly once' zijn niet nodig: bedrijfsprocessen zijn meestal niet gevoelig voor het opnieuw ontvangen van berichten of evenementen, en in bijzondere gevallen waar dat belangrijk is, is het eenvoudiger om een extra controle in de bedrijfslogica toe te voegen dan voortdurend voldoende 'dure' garanties te gebruiken;
  • berichten en signalen moeten worden betrokken bij de algemene transactie met de wijziging van de status van bedrijfsprocessen en domeingegevens. De voorkeur gaat uit naar het gebruik van het patroon Transactionele Outbox, maar dit vereist een extra tabel in de database en een retranslater. In JEE-toepassingen kan dit worden vereenvoudigd met behulp van een lokale JTA-manager, maar de verbinding met de gekozen broker moet in de modus kunnen werken XA;
  • de handlers voor binnenkomende berichten en evenementen moeten ook werken met de transactie van de wijziging van de status van het bedrijfsproces: als deze transactie wordt teruggedraaid, moet ook de ontvangst van het bericht worden geannuleerd;
  • berichten die niet konden worden afgeleverd vanwege fouten, moeten in een aparte opslag worden geplaatst DLQ (Dead Letter Queue). We have created a separate platform microservice that stores such messages in its storage, indexes them by attributes (for quick grouping and searching), and provides an API for viewing, resending to the destination address, and deleting messages. System administrators can work with this service through their web interface;
  • in the broker settings, you need to adjust the number of delivery retries and delays between deliveries to reduce the chances of messages ending up in the DLQ (calculating optimal parameters is practically impossible, but one can empirically adjust them during operation);
  • the DLQ storage must be continuously monitored, and the monitoring system should notify system administrators so that they can respond as quickly as possible when undelivered messages appear. This will help reduce the 'impact zone' of an occurring failure or business logic error;
  • the integration bus should be insensitive to the temporary absence of applications: subscriptions to the topic must be durable, and the application domain name must be unique so that during the application's absence, its messages are not processed by someone else from the queue.

Ensuring thread safety of business logic

The same instance of a business process may receive multiple messages and events simultaneously, processing of which will start in parallel. Meanwhile, everything should be simple and thread-safe for the application developer.

The business logic of the process handles each external event affecting this business process individually. Such events may include:

  • starting an instance of the business process;
  • user action related to activity within the business process;
  • receipt of a message or signal to which the instance of the business process is subscribed;
  • triggering a timer set by the instance of the business process;
  • controlling influence through the API (e.g., emergency interruption of the process).

Elk dergelijk evenement kan de status van een bedrijfsprocesinstance veranderen: sommige activiteiten kunnen eindigen en andere kunnen beginnen, en de waarden van persistente eigenschappen kunnen veranderen. Het sluiten van elke activiteit kan leiden tot de activatie van een of meer volgende activiteiten. Deze kunnen op hun beurt stoppen en wachten op andere gebeurtenissen, of, als ze geen extra gegevens nodig hebben, kunnen ze eindigen in dezelfde transactie. Voor het sluiten van de transactie wordt de nieuwe status van het bedrijfsproces opgeslagen in de database, waar deze zal wachten op de volgende externe gebeurtenis.

Persistente gegevens van het bedrijfsproces, opgeslagen in een relationele database, zijn een zeer handige synchronisitiepunt voor verwerking, als je SELECT FOR UPDATE gebruikt. Als ƩƩn transactie erin slaagt de status van het bedrijfsproces uit de database te verkrijgen voor wijziging, kan geen andere transactie dezezelfde status tegelijkertijd verkrijgen voor een andere wijziging. Na het beƫindigen van de eerste transactie zal de tweede gegarandeerd de al gewijzigde status ontvangen.

Door pessimistische blokkeringen aan de kant van het DBMS toe te passen, voldoen we aan alle vereiste voorwaarden ACID, en behouden we de mogelijkheid om de applicatie met bedrijfslogica op te schalen door het aantal draaiende instanties te vergroten.

Echter, pessimistische blokkeringen kunnen ons blootstellen aan deadlocks, en daarom moet SELECT FOR UPDATE toch beperkt worden tot een redelijke time-out voor het geval er deadlocks optreden in verontrustende situaties in de bedrijfslogica.

Een ander probleem is de synchronisatie van de start van het bedrijfsproces. Totdat er een instantie van het bedrijfsproces is, is er ook geen status in de database, waardoor de beschreven methode niet geschikt is. Als je uniciteit van de instantie van het bedrijfsproces binnen een bepaalde scope wilt waarborgen, is er een synchronisatieobject nodig dat is geassocieerd met de procesklasse en de bijbehorende scope. Om dit probleem op te lossen, gebruiken we een ander mechanisme voor blokkeringen, waarmee we een blokkering op een willekeurige hulpbron kunnen verkrijgen, gespecificeerd door een sleutel in URI-formaat, via een externe service.

In onze voorbeelden bevat het bedrijfsproces InitialPlayer een verklaring

uniqueConstraint = UniqueConstraints.singleton

Daarom zijn er in de log berichten over het vastzetten en loslaten van de bijbehorende sleutel. Voor andere bedrijfsprocessen zijn dergelijke berichten er niet: uniqueConstraint is niet vastgesteld.

Problemen met bedrijfsprocessen met een persistentie status

Soms helpt een persistentie status niet alleen, maar hinder het ook enorm tijdens de ontwikkeling.
De problemen beginnen wanneer er wijzigingen moeten worden aangebracht in de bedrijfslogica en/of het model van het bedrijfsproces. Niet elke wijziging is compatibel met de oude staat van de bedrijfsprocessen. Als er veel 'levende' exemplaren in de database zijn, kan het aanbrengen van incompatibele wijzigingen veel problemen opleveren, waar we vaak tegenaan lopen bij het gebruik van jBPM.

Afhankelijk van de diepte van de wijzigingen kunnen er twee paden worden gevolgd:

  1. een nieuw type bedrijfsproces creƫren om incompatibele wijzigingen in het oude te vermijden en deze te gebruiken in plaats van het oude bij het starten van nieuwe exemplaren. De oude exemplaren blijven 'op de oude manier' werken;
  2. de persistentie status van de bedrijfsprocessen migreren bij het bijwerken van de bedrijfslogica.

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

  • duplicatie van bedrijfslogica in veel modellen van bedrijfsprocessen, toename van de omvang van de bedrijfslogica;
  • vaak is een directe overstap naar de nieuwe bedrijfslogica vereist (in integratietaken - bijna altijd);
  • de ontwikkelaar weet niet op welk moment verouderde modellen kunnen worden verwijderd.

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

  • in de database wordt de persistentie status van het bedrijfsproces opgeslagen in een gemakkelijk leesbaar en verwerkbaar formaat: als een JSON-string. Dit stelt ons in staat om migraties zowel binnen de applicatie als daarbuiten uit te voeren. In het uiterste geval kan je het zelfs handmatig aanpassen (bijzonder nuttig tijdens de ontwikkeling en debugging);
  • de integratieve bedrijfslogica gebruikt geen namen van bedrijfsprocessen, zodat op elk moment de implementatie van een van de betrokken processen kan worden vervangen door een nieuwe, met een nieuwe naam (bijvoorbeeld 'InitialPlayerV2'). De koppeling gebeurt via de namen van berichten en signalen;
  • Het procesmodel heeft een versienummer dat we verhogen wanneer we onverenigbare wijziging aanbrengen in dit model, en dit nummer wordt bewaard samen met de status van de procesinstantie;
  • De persistente status van het proces wordt eerst uit de database gelezen in een handige objectmodel waarmee de migratieprocedure kan werken, als het versienummer van het model is gewijzigd;
  • De migratieprocedure wordt naast de businesslogica geplaatst en wordt 'lui' aangeroepen voor elke instantie van het bedrijfsproces op het moment dat deze wordt hersteld uit de database;
  • Als het nodig is om de status van alle procesinstanties snel en synchroon te migreren, worden meer klassieke database migratieoplossingen toegepast, maar daarbij moet met JSON worden gewerkt.

Is er nog een framework nodig voor bedrijfsprocessen?

De oplossingen die in het artikel worden beschreven, hebben ons in staat gesteld ons leven aanzienlijk te vereenvoudigen, de reikwijdte van vragen die op het niveau van applicatieontwikkeling kunnen worden opgelost uit te breiden en de ideeƫn voor het scheiden van businesslogica in microservices aantrekkelijker te maken. Hiervoor is veel werk verzet, en er is een zeer 'lichtgewicht' framework voor bedrijfsprocessen gecreƫerd, evenals hulpelementen voor het oplossen van de aangeduide problemen in de context van een breed scala aan applicatietaken. We hebben de wens om deze resultaten te delen en de ontwikkeling van algemene componenten openbaar te maken onder een vrije licentie. Dit zal bepaalde inspanningen en tijd vereisen. Het begrijpen van de vraag naar dergelijke oplossingen zou voor ons een extra motivatie kunnen zijn. In het voorgestelde artikel wordt er weinig aandacht besteed aan de mogelijkheden van het framework zelf, maar enkele daarvan zijn zichtbaar uit de gepresenteerde voorbeelden. Als we ons framework toch publiceren, zal dit een apart artikel krijgen. Voor nu zouden we dankbaar zijn als je een kleine feedback achterlaat door de vraag te beantwoorden:

Alleen geregistreerde gebruikers kunnen deelnemen aan de enquĆŖte. Log in, alstublieft.

Is er nog een framework nodig voor bedrijfsprocessen?

  • 18,8%Ja, we zijn al een tijd op zoek naar iets dergelijks3

  • 12,5%Ik ben benieuwd naar uw implementatie, misschien kan het nuttig zijn2

  • 6,2%We gebruiken een van de bestaande frameworks, maar denken na over vervanging1

  • 18,8%We gebruiken een van de bestaande frameworks, alles bevalt3

  • 18,8%We redden het zonder framework3

  • 25,0%We schrijven onze eigen4

16 gebruikers hebben gestemd. 7 gebruikers hebben zich onthouden.

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers šŸ”„ Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster