Integració d'estil BPM

Integració d'estil BPM

Hola Habr!

La nostra empresa està especialitzada en el desenvolupament de solucions de programari de classe ERP, la part del lleó de les quals està ocupada per sistemes transaccionals amb una gran quantitat de lògica de negoci i flux de documents a la EDMS. Les versions actuals dels nostres productes es basen en tecnologies JavaEE, però també estem experimentant activament amb microserveis. Una de les àrees més problemàtiques d'aquestes solucions és la integració de diversos subsistemes pertanyents a dominis adjacents. Els problemes d'integració sempre ens han donat un gran maldecap, independentment dels estils arquitectònics, les piles tecnològiques i els marcs que utilitzem, però recentment s'ha avançat en la resolució d'aquests problemes.

En l'article que us crito l'atenció, us parlaré de l'experiència i recerca arquitectònica que té NPO Krista a l'àrea designada. També veurem un exemple de solució senzilla a un problema d'integració des del punt de vista d'un desenvolupador d'aplicacions i descobrirem què s'amaga darrere d'aquesta senzillesa.

Exempció de responsabilitat

Les solucions arquitectòniques i tècniques descrites a l'article les proposo a partir de l'experiència personal en el context de tasques específiques. Aquestes solucions no pretenen ser universals i poden no ser òptimes en altres condicions d'ús.

Què hi té a veure el BPM?

Per respondre a aquesta pregunta, hem d'aprofundir una mica més en els detalls dels problemes aplicats de les nostres solucions. La part principal de la lògica de negoci en el nostre sistema transaccional típic és introduir dades a la base de dades mitjançant interfícies d'usuari, verificació manual i automatitzada d'aquestes dades, dur-la a terme a través d'algun flux de treball, publicar-les a un altre sistema / base de dades analítica / arxiu, generar informes. . Així, la funció clau del sistema per als clients és l'automatització dels seus processos de negoci interns.

Per comoditat, utilitzem el terme "document" en comunicació com una abstracció d'un conjunt de dades unides per una clau comuna a la qual es pot "enllaçar" un determinat flux de treball.
Però, què passa amb la lògica d'integració? Després de tot, la tasca d'integració la genera l'arquitectura del sistema, que es "talla" en parts NO a petició del client, sinó sota la influència de factors completament diferents:

  • subjecte a la llei de Conway;
  • com a conseqüència de la reutilització de subsistemes desenvolupats prèviament per a altres productes;
  • a criteri de l'arquitecte, en funció de requisits no funcionals.

Hi ha una gran temptació de separar la lògica d'integració de la lògica de negoci del flux de treball principal, per no contaminar la lògica de negoci amb artefactes d'integració i salvar el desenvolupador d'aplicacions de la necessitat d'aprofundir en les característiques del paisatge arquitectònic del sistema. Aquest enfocament té diversos avantatges, però la pràctica demostra la seva ineficàcia:

  • la resolució de problemes d'integració sol recaure en les opcions més senzilles en forma de trucades síncrones a causa dels punts d'extensió limitats en la implementació del flux de treball principal (els desavantatges de la integració síncrona es discuteixen a continuació);
  • els artefactes d'integració encara penetren en la lògica empresarial bàsica quan es requereix retroalimentació d'un altre subsistema;
  • el desenvolupador de l'aplicació ignora la integració i pot trencar-la fàcilment canviant el flux de treball;
  • el sistema deixa de ser un tot únic des del punt de vista de l'usuari, les “costures” entre subsistemes es fan notoris i apareixen operacions redundants d'usuari, iniciant la transferència de dades d'un subsistema a un altre.

Un altre enfocament és considerar les interaccions d'integració com a part integral de la lògica i el flux de treball bàsics del negoci. Per evitar que les qualificacions dels desenvolupadors d'aplicacions es disparin, la creació de noves interaccions d'integració hauria de ser fàcil i sense esforç, amb una oportunitat mínima per triar una solució. Això és més difícil de fer del que sembla: l'eina ha de ser prou potent com per oferir a l'usuari la varietat d'opcions necessàries per al seu ús, sense que li permeti "disparar-se al peu". Hi ha moltes preguntes que un enginyer ha de respondre en el context de les tasques d'integració, però que un desenvolupador d'aplicacions no hauria de pensar en el seu treball diari: límits de transaccions, coherència, atomicitat, seguretat, escalat, càrrega i distribució de recursos, encaminament, marshaling, etc. contextos de distribució i canvi, etc. Cal oferir als desenvolupadors d'aplicacions plantilles de solució bastant senzilles en les quals les respostes a totes aquestes preguntes ja estiguin amagades. Aquestes plantilles han de ser força segures: la lògica empresarial canvia molt sovint, la qual cosa augmenta el risc d'introduir errors, el cost dels errors s'ha de mantenir en un nivell força baix.

Però què hi té a veure BPM? Hi ha moltes opcions per implementar el flux de treball...
De fet, una altra implementació de processos empresarials és molt popular a les nostres solucions: mitjançant la definició declarativa d'un diagrama de transició d'estats i la connexió dels controladors amb la lògica empresarial per a les transicions. En aquest cas, l'estat que determina la posició actual del "document" en el procés empresarial és un atribut del propi "document".

Integració d'estil BPM
Així es veu el procés a l'inici d'un projecte

La popularitat d'aquesta implementació es deu a la relativa simplicitat i velocitat de creació de processos empresarials lineals. Tanmateix, a mesura que els sistemes de programari esdevenen més complexos, la part automatitzada del procés empresarial creix i es fa més complexa. Hi ha una necessitat de descomposició, reutilització de parts dels processos, així com processos de ramificació perquè cada branca s'executi en paral·lel. En aquestes condicions, l'eina esdevé incòmoda i el diagrama de transició d'estat perd el seu contingut d'informació (les interaccions d'integració no es reflecteixen en absolut al diagrama).

Integració d'estil BPM
Així és el procés després de diverses iteracions d'aclariment de requisits.

La sortida a aquesta situació va ser la integració del motor jBPM en alguns productes amb els processos empresarials més complexos. A curt termini, aquesta solució va tenir un cert èxit: va ser possible implementar processos de negoci complexos mantenint un diagrama bastant informatiu i rellevant en la notació. BPMN2.

Integració d'estil BPM
Una petita part d'un procés empresarial complex

A llarg termini, la solució no va estar a l'altura de les expectatives: l'elevada intensitat laboral de la creació de processos empresarials a través d'eines visuals no va permetre assolir indicadors de productivitat acceptables, i l'eina en si es va convertir en una de les més desagradables entre els desenvolupadors. També hi va haver queixes sobre l'estructura interna del motor, que va provocar l'aparició de molts "pegats" i "mulletes".

El principal aspecte positiu de l'ús de jBPM va ser la consciència dels beneficis i els perjudicis de tenir un estat persistent d'una instància de procés de negoci. També vam veure la possibilitat d'utilitzar un enfocament de procés per implementar protocols d'integració complexos entre diferents aplicacions mitjançant interaccions asíncrones mitjançant senyals i missatges. La presència d'un estat persistent juga un paper crucial en això.

A partir de l'anterior, podem concloure: L'enfocament de processos a l'estil BPM ens permet resoldre una àmplia gamma de tasques per automatitzar processos empresarials cada cop més complexos, ajustar harmònicament les activitats d'integració en aquests processos i mantenir la capacitat de mostrar visualment el procés implementat en una notació adequada.

Inconvenients de les trucades síncrones com a patró d'integració

La integració sincrònica fa referència a la trucada de bloqueig més senzilla. Un subsistema actua com a servidor i exposa l'API amb el mètode necessari. Un altre subsistema actua com a client i en el moment adequat fa una trucada i espera el resultat. Depenent de l'arquitectura del sistema, els costats del client i el servidor es poden localitzar en la mateixa aplicació i procés, o bé en diferents. En el segon cas, haureu d'aplicar alguna implementació RPC i proporcionar la classificació dels paràmetres i el resultat de la trucada.

Integració d'estil BPM

Aquest patró d'integració té un conjunt bastant gran d'inconvenients, però s'utilitza molt a la pràctica per la seva senzillesa. La rapidesa d'implementació captiva i obliga a utilitzar-la una i altra vegada davant els terminis urgents, registrant la solució com a deute tècnic. Però també passa que els desenvolupadors sense experiència l'utilitzen inconscientment, simplement sense adonar-se de les conseqüències negatives.

A més de l'augment més evident de la connectivitat del subsistema, també hi ha problemes menys evidents amb les transaccions de "creixement" i "estirament". De fet, si la lògica empresarial fa alguns canvis, les transaccions no es poden evitar i les transaccions, al seu torn, bloquegen determinats recursos de l'aplicació afectats per aquests canvis. És a dir, fins que un subsistema no espere una resposta de l'altre, no podrà completar la transacció i eliminar els bloquejos. Això augmenta significativament el risc de diversos efectes:

  • Es perd la capacitat de resposta del sistema, els usuaris esperen molt de temps les respostes a les sol·licituds;
  • el servidor generalment deixa de respondre a les sol·licituds dels usuaris a causa d'un grup de fils sobreocupació: la majoria dels fils estan bloquejats en un recurs ocupat per una transacció;
  • Comencen a aparèixer bloquejos: la probabilitat que es produeixin depèn en gran mesura de la durada de les transaccions, la quantitat de lògica empresarial i els bloquejos implicats en la transacció;
  • apareixen errors de temps d'espera de transacció;
  • el servidor "falla" amb OutOfMemory si la tasca requereix processar i canviar grans quantitats de dades, i la presència d'integracions sincròniques fa que sigui molt difícil dividir el processament en transaccions "més lleugeres".

Des d'un punt de vista arquitectònic, l'ús de trucades de bloqueig durant la integració comporta una pèrdua de control sobre la qualitat dels subsistemes individuals: és impossible garantir els indicadors de qualitat objectiu d'un subsistema aïllament dels indicadors de qualitat d'un altre subsistema. Si els subsistemes són desenvolupats per diferents equips, aquest és un gran problema.

Les coses es tornen encara més interessants si els subsistemes que s'integren es troben en aplicacions diferents i cal fer canvis sincrònics en ambdós costats. Com garantir la transaccionalitat d'aquests canvis?

Si es fan canvis en transaccions separades, haureu de proporcionar una gestió d'excepcions i una compensació fiables, i això elimina completament el principal avantatge de les integracions sincròniques: la simplicitat.

També ens vénen al cap les transaccions distribuïdes, però no les fem servir en les nostres solucions: és difícil garantir la fiabilitat.

"Saga" com a solució al problema de la transacció

Amb la creixent popularitat dels microserveis, la demanda de Patró de saga.

Aquest patró soluciona perfectament els problemes esmentats anteriorment de les transaccions llargues i també amplia les capacitats de gestionar l'estat del sistema des de la lògica empresarial: la compensació després d'una transacció fallida pot no tornar el sistema al seu estat original, però proporcionar una ruta alternativa de tractament de dades. Això també us permet evitar repetir els passos de processament de dades completats amb èxit quan intenteu portar el procés a un "bon" final.

Curiosament, en els sistemes monolítics aquest patró també és rellevant quan es tracta de la integració de subsistemes poc acoblats i s'observen efectes negatius causats per transaccions de llarga durada i els corresponents bloquejos de recursos.

En relació amb els nostres processos de negoci a l'estil BPM, resulta molt fàcil implementar "Sagas": els passos individuals de la "Saga" es poden especificar com a activitats dins del procés de negoci, i l'estat persistent del procés de negoci també determina l'estat intern de la "Saga". És a dir, no necessitem cap mecanisme de coordinació addicional. Tot el que necessiteu és un agent de missatges que admeti garanties "almenys una vegada" com a transport.

Però aquesta solució també té el seu propi "preu":

  • la lògica de negoci es fa més complexa: cal treballar la compensació;
  • caldrà abandonar la consistència total, que pot ser especialment sensible per als sistemes monolítics;
  • L'arquitectura es fa una mica més complicada i apareix una necessitat addicional d'un intermediari de missatges;
  • es requeriran eines addicionals de supervisió i administració (tot i que en general això és bo: la qualitat del servei del sistema augmentarà).

Per als sistemes monolítics, la justificació per utilitzar "Sag" no és tan òbvia. Per als microserveis i altres SOA, on el més probable és que ja hi hagi un corredor, i la coherència total es sacrifica a l'inici del projecte, els avantatges d'utilitzar aquest patró poden superar significativament els desavantatges, especialment si hi ha una API convenient a la lògica empresarial. nivell.

Encapsular la lògica empresarial en microserveis

Quan vam començar a experimentar amb microserveis, va sorgir una pregunta raonable: on col·locar la lògica de negoci del domini en relació amb el servei que garanteix la persistència de les dades del domini?

Quan es mira l'arquitectura de diversos BPMS, pot semblar raonable separar la lògica empresarial de la persistència: crear una capa de microserveis independents de la plataforma i del domini que formen un entorn i un contenidor per executar la lògica empresarial del domini i dissenyar la persistència de les dades del domini com a una capa separada de microserveis molt senzills i lleugers. Els processos empresarials en aquest cas realitzen l'orquestració dels serveis de la capa de persistència.

Integració d'estil BPM

Aquest enfocament té un avantatge molt gran: podeu augmentar la funcionalitat de la plataforma tant com vulgueu i només la capa corresponent de microserveis de la plataforma es convertirà en "grossa" a partir d'això. Els processos empresarials de qualsevol domini poden utilitzar immediatament la nova funcionalitat de la plataforma tan bon punt s'actualitza.

Un estudi més detallat va revelar els desavantatges significatius d'aquest enfocament:

  • un servei de plataforma que executa la lògica de negoci de molts dominis alhora comporta grans riscos com a punt de fallada únic. Els canvis freqüents a la lògica empresarial augmenten el risc d'errors que condueixin a fallades a tot el sistema;
  • problemes de rendiment: la lògica empresarial treballa amb les seves dades mitjançant una interfície estreta i lenta:
    • les dades tornaran a ser agrupades i bombejades a través de la pila de xarxa;
    • un servei de domini sovint proporcionarà més dades de les que es requereixen perquè la lògica empresarial processi a causa de les capacitats insuficients per parametritzar les sol·licituds a nivell de l'API externa del servei;
    • diverses peces independents de la lògica empresarial poden tornar a sol·licitar repetidament les mateixes dades per processar-les (aquest problema es pot mitigar afegint components de sessió que emmagatzemen les dades a la memòria cau, però això complica encara més l'arquitectura i crea problemes de rellevància de les dades i invalidació de la memòria cau);
  • problemes de transaccions:
    • els processos empresarials amb estat persistent, que s'emmagatzema un servei de plataforma, són incoherents amb les dades del domini i no hi ha maneres fàcils de resoldre aquest problema;
    • col·locar el bloqueig de dades del domini fora de la transacció: si la lògica de negoci del domini ha de fer canvis després de comprovar primer la correcció de les dades actuals, cal excloure la possibilitat d'un canvi competitiu en les dades processades. El bloqueig de dades externes pot ajudar a resoldre el problema, però aquesta solució comporta riscos addicionals i redueix la fiabilitat global del sistema;
  • dificultats addicionals en l'actualització: en alguns casos, el servei de persistència i la lògica de negoci s'han d'actualitzar de manera sincrònica o en seqüència estricta.

En última instància, vam haver de tornar al bàsic: encapsular les dades del domini i la lògica empresarial del domini en un sol microservei. Aquest enfocament simplifica la percepció d'un microservei com a component integral del sistema i no dóna lloc als problemes anteriors. Això tampoc es dóna de manera gratuïta:

  • L'estandardització de l'API és necessària per a la interacció amb la lògica empresarial (en particular, per oferir activitats d'usuari com a part dels processos empresarials) i els serveis de la plataforma API; requereix una atenció més acurada als canvis de l'API, la compatibilitat cap endavant i cap enrere;
  • és necessari afegir biblioteques d'execució addicionals per garantir el funcionament de la lògica empresarial com a part de cada microservei, i això genera nous requisits per a aquestes biblioteques: lleugeresa i un mínim de dependències transitives;
  • Els desenvolupadors de lògica empresarial han de supervisar les versions de les biblioteques: si un microservei no s'ha finalitzat durant molt de temps, és probable que contingui una versió obsoleta de les biblioteques. Això pot ser un obstacle inesperat per afegir una funció nova i pot requerir migrar la lògica empresarial antiga d'aquest servei a noves versions de biblioteques si hi hagués canvis incompatibles entre les versions.

Integració d'estil BPM

Una capa de serveis de plataforma també està present en aquesta arquitectura, però aquesta capa ja no forma un contenidor per executar la lògica empresarial del domini, sinó només el seu entorn, proporcionant funcions auxiliars de "plataforma". Aquesta capa es necessita no només per mantenir la naturalesa lleugera dels microserveis de domini, sinó també per centralitzar la gestió.

Per exemple, les activitats dels usuaris en els processos empresarials generen tasques. Tanmateix, quan es treballa amb tasques, l'usuari ha de veure tasques de tots els dominis a la llista general, el que significa que hi ha d'haver un servei de registre de tasques de la plataforma corresponent, sense la lògica empresarial del domini. Mantenir l'encapsulació de la lògica empresarial en aquest context és bastant problemàtic, i aquest és un altre compromís d'aquesta arquitectura.

Integració de processos de negoci a través dels ulls d'un desenvolupador d'aplicacions

Com s'ha esmentat anteriorment, un desenvolupador d'aplicacions s'ha d'abstraure de les característiques tècniques i d'enginyeria d'implementar la interacció de diverses aplicacions perquè es pugui comptar amb una bona productivitat de desenvolupament.

Intentem resoldre un problema d'integració força difícil, inventat especialment per a l'article. Aquesta serà una tasca de “joc” que implicarà tres aplicacions, on cadascuna d'elles defineix un determinat nom de domini: “app1”, “app2”, “app3”.

Dins de cada aplicació, s'inicien processos de negoci que comencen a “jugar a pilota” a través del bus d'integració. Els missatges amb el nom "Pilota" actuaran com una bola.

Regles del joc:

  • el primer jugador és l'iniciador. Convida altres jugadors al joc, comença el joc i pot acabar-lo en qualsevol moment;
  • els altres jugadors declaren la seva participació en el joc, "coneixeu-vos" entre ells i el primer jugador;
  • després de rebre la pilota, el jugador selecciona un altre jugador participant i li passa la pilota. Es compta el nombre total de transmissions;
  • Cada jugador té "energia" que disminueix amb cada passada de pilota d'aquest jugador. Quan s'esgota l'energia, el jugador abandona el joc, anunciant la seva renúncia;
  • si el jugador es queda sol, immediatament anuncia la seva marxa;
  • Quan tots els jugadors són eliminats, el primer jugador declara el joc acabat. Si abandona el joc abans d'hora, queda per seguir el joc per completar-lo.

Per resoldre aquest problema, utilitzaré el nostre DSL per a processos de negoci, que ens permet descriure la lògica a Kotlin de manera compacta, amb un mínim de boilerplate.

El procés de negoci del primer jugador (també conegut com l'iniciador del joc) funcionarà a l'aplicació app1:

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

A més d'executar la lògica de negoci, el codi anterior pot produir un model d'objectes d'un procés de negoci, que es pot visualitzar en forma de diagrama. Encara no hem implementat el visualitzador, així que hem hagut de dedicar una mica de temps dibuixant (aquí he simplificat lleugerament la notació BPMN pel que fa a l'ús de portes per millorar la coherència del diagrama amb el codi següent):

Integració d'estil BPM

app2 inclourà el procés de negoci de l'altre jugador:

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

Diagrama:

Integració d'estil BPM

A l'aplicació app3 farem un jugador amb un comportament una mica diferent: en lloc de seleccionar aleatòriament el següent jugador, actuarà segons l'algorisme de ronda:

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

En cas contrari, el comportament del jugador no difereix de l'anterior, de manera que el diagrama no canvia.

Ara necessitem una prova per executar tot això. Donaré només el codi de la prova en si, per tal de no desordenar l'article amb un boilerplate (de fet, vaig utilitzar l'entorn de prova creat anteriorment per provar la integració d'altres processos empresarials):

testGame()

@Test
public void testGame() throws InterruptedException {
    String pl2 = startProcess(app2, "RandomPlayer", playerParams("Player2", 20));
    String pl3 = startProcess(app2, "RandomPlayer", playerParams("Player3", 40));
    String pl4 = startProcess(app3, "RoundRobinPlayer", playerParams("Player4", 25));
    String pl5 = startProcess(app3, "RoundRobinPlayer", playerParams("Player5", 35));
    String pl1 = startProcess(app1, "InitialPlayer");
    // Теперь нужно немного подождать, пока игроки "познакомятся" друг с другом.
    // Ждать через sleep - плохое решение, зато самое простое. 
    // Не делайте так в серьезных тестах!
    Thread.sleep(1000);
    // Запускаем игру, закрывая пользовательскую активность
    assertTrue(closeTask(app1, pl1, "Start"));
    app1.getWaiting().waitProcessFinished(pl1);
    app2.getWaiting().waitProcessFinished(pl2);
    app2.getWaiting().waitProcessFinished(pl3);
    app3.getWaiting().waitProcessFinished(pl4);
    app3.getWaiting().waitProcessFinished(pl5);
}

private Map<String, Object> playerParams(String name, int energy) {
    Map<String, Object> params = new HashMap<>();
    params.put("playerName", name);
    params.put("energy", energy);
    return params;
}

Anem a fer la prova i mirem el registre:

sortida de la consola

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

De tot això podem extreure diverses conclusions importants:

  • amb les eines necessàries, els desenvolupadors d'aplicacions poden crear interaccions d'integració entre aplicacions sense interrompre la lògica empresarial;
  • la complexitat d'una tasca d'integració que requereix competències d'enginyeria es pot amagar dins del marc si aquesta s'inclou inicialment a l'arquitectura del marc. La dificultat d'un problema no es pot amagar, de manera que la solució d'un problema difícil en codi semblarà;
  • A l'hora de desenvolupar la lògica d'integració, és imprescindible tenir en compte la consistència eventual i la manca de linearització dels canvis en l'estat de tots els participants de la integració. Això ens obliga a complicar la lògica per tal de fer-la insensible a l'ordre en què es produeixen els esdeveniments externs. En el nostre exemple, el jugador es veu obligat a participar en el joc després de declarar la seva sortida del joc: els altres jugadors continuaran passant-li la pilota fins que arribi la informació sobre la seva sortida i sigui processada per tots els participants. Aquesta lògica no segueix les regles del joc i és una solució de compromís en el marc de l'arquitectura escollida.

A continuació, parlarem de les diferents complexitats de la nostra solució, compromisos i altres punts.

Tots els missatges estan en una cua

Totes les aplicacions integrades funcionen amb un bus d'integració, que es presenta en forma d'un intermediari extern, un BPMQueue per als missatges i un tema BPMTopic per a senyals (esdeveniments). Posar tots els missatges a una cua és en si mateix un compromís. A nivell de lògica empresarial, ara podeu introduir tants tipus de missatges nous com vulgueu sense fer canvis a l'estructura del sistema. Això és una simplificació important, però comporta certs riscos, que en el context de les nostres tasques típiques no ens semblaven tan significatius.

Integració d'estil BPM

No obstant això, aquí hi ha una subtilesa: cada aplicació filtra "els seus" missatges de la cua de l'entrada, pel nom del seu domini. El domini també es pot especificar en senyals si cal limitar l'"abast de visibilitat" del senyal a una sola aplicació. Això hauria d'augmentar el rendiment del bus, però la lògica empresarial ara ha d'operar amb noms de domini: per adreçar missatges -obligatori, per a senyals- desitjable.

Garantir la fiabilitat del bus d'integració

La fiabilitat consta de diversos punts:

  • L'agent de missatges seleccionat és un component crític de l'arquitectura i un únic punt de fallada: ha de ser prou tolerant a errors. Només hauríeu d'utilitzar implementacions provades en el temps, amb un bon suport i una gran comunitat;
  • cal garantir una alta disponibilitat de l'agent de missatges, per a la qual cosa s'ha de separar físicament de les aplicacions integrades (l'alta disponibilitat d'aplicacions amb lògica de negoci aplicada és molt més difícil i car de garantir);
  • el corredor està obligat a oferir "almenys una vegada" garanties de lliurament. Aquest és un requisit obligatori per al funcionament fiable del bus d'integració. No calen garanties de nivell "exactament una vegada": els processos empresarials, per regla general, no són sensibles a l'arribada repetida de missatges o esdeveniments, i en tasques especials on això és important, és més fàcil afegir controls addicionals al negoci. lògica que utilitzar constantment garanties bastant "cares";
  • L'enviament de missatges i senyals ha d'estar implicat en una transacció global amb canvis en l'estat dels processos empresarials i les dades del domini. L'opció preferida seria utilitzar un patró Bústia de sortida transaccional, però requerirà una taula addicional a la base de dades i un repetidor. A les aplicacions JEE, això es pot simplificar utilitzant un gestor JTA local, però la connexió amb l'agent seleccionat ha de poder funcionar en XA;
  • els gestors de missatges i esdeveniments entrants també han de treballar amb una transacció que canvia l'estat d'un procés de negoci: si aquesta transacció es torna enrere, s'ha de cancel·lar la recepció del missatge;
  • Els missatges que no s'han pogut lliurar a causa d'errors s'han d'emmagatzemar en un emmagatzematge independent D.L.Q. (Cua de lletres mortes). Amb aquesta finalitat, hem creat un microservei de plataforma independent que emmagatzema aquests missatges al seu emmagatzematge, els indexa per atributs (per a l'agrupació i la cerca ràpides) i exposa una API per visualitzar-los, tornar-los a enviar a l'adreça de destinació i suprimir missatges. Els administradors del sistema poden treballar amb aquest servei mitjançant la seva interfície web;
  • a la configuració del corredor, cal ajustar el nombre de reintents de lliurament i els retards entre lliuraments per tal de reduir la probabilitat que els missatges entrin a DLQ (és gairebé impossible calcular els paràmetres òptims, però podeu actuar de manera empírica i ajustar-los durant l'operació). );
  • El magatzem DLQ s'ha de supervisar contínuament i el sistema de monitorització ha d'avisar els administradors del sistema perquè quan es produeixin missatges no lliurats, puguin respondre el més ràpidament possible. Això reduirà l'"àrea afectada" d'una fallada o d'un error de lògica empresarial;
  • el bus d'integració ha de ser insensible a l'absència temporal d'aplicacions: les subscripcions a un tema han de ser duradores i el nom de domini de l'aplicació ha de ser únic perquè mentre l'aplicació estigui absent, algú altre no intentarà processar els seus missatges des de l'aplicació. cua.

Garantir la seguretat dels fils de la lògica empresarial

La mateixa instància d'un procés de negoci pot rebre diversos missatges i esdeveniments alhora, el processament dels quals començarà en paral·lel. Al mateix temps, per a un desenvolupador d'aplicacions, tot hauria de ser senzill i segur.

La lògica de negoci d'un procés processa cada esdeveniment extern que afecta aquest procés de negoci individualment. Aquests esdeveniments podrien ser:

  • llançament d'una instància de procés de negoci;
  • acció de l'usuari relacionada amb l'activitat dins d'un procés de negoci;
  • recepció d'un missatge o senyal al qual està subscrita una instància de procés de negoci;
  • activació d'un temporitzador establert per una instància de procés de negoci;
  • acció de control mitjançant API (per exemple, interrupció del procés).

Cadascun d'aquests esdeveniments pot canviar l'estat d'una instància de procés de negoci: algunes activitats poden acabar i altres poden començar, i els valors de les propietats persistents poden canviar. El tancament de qualsevol activitat pot comportar l'activació d'una o més de les activitats següents. Aquests, al seu torn, poden deixar d'esperar altres esdeveniments o, si no necessiten cap dada addicional, poden completar la mateixa transacció. Abans de tancar la transacció, el nou estat del procés empresarial es desa a la base de dades, on s'esperarà que es produeixi el següent esdeveniment extern.

Les dades persistents del procés de negoci emmagatzemades en una base de dades relacional són un punt molt convenient per sincronitzar el processament si feu servir SELECT FOR UPDATE. Si una transacció va aconseguir obtenir l'estat d'un procés de negoci a partir de la base per canviar-lo, llavors cap altra transacció en paral·lel podrà obtenir el mateix estat per a un altre canvi, i després de la finalització de la primera transacció, la segona és garantit per rebre l'estat ja canviat.

Utilitzant bloquejos pessimistes al costat del SGBD, complim tots els requisits necessaris ÀCID, i també conserva la capacitat d'escalar l'aplicació amb la lògica empresarial augmentant el nombre d'instàncies en execució.

Tanmateix, els bloquejos pessimistes ens amenacen amb bloquejos, la qual cosa significa que SELECT FOR UPDATE encara s'hauria de limitar a un temps d'espera raonable en cas que es produeixin bloquejos en alguns casos flagrants de la lògica empresarial.

Un altre problema és la sincronització de l'inici d'un procés de negoci. Tot i que no hi ha cap instància d'un procés de negoci, no hi ha cap estat a la base de dades, de manera que el mètode descrit no funcionarà. Si necessiteu assegurar la singularitat d'una instància de procés de negoci en un àmbit específic, necessitareu algun tipus d'objecte de sincronització associat a la classe de procés i l'àmbit corresponent. Per solucionar aquest problema, utilitzem un mecanisme de bloqueig diferent que ens permet bloquejar un recurs arbitrari especificat per una clau en format URI mitjançant un servei extern.

En els nostres exemples, el procés empresarial InitialPlayer conté una declaració

uniqueConstraint = UniqueConstraints.singleton

Per tant, el registre conté missatges sobre agafar i alliberar el bloqueig de la clau corresponent. No hi ha aquests missatges per a altres processos empresarials: uniqueConstraint no està establert.

Problemes dels processos de negoci amb estat persistent

De vegades, tenir un estat persistent no només ajuda, sinó que també dificulta el desenvolupament.
Els problemes comencen quan cal fer canvis a la lògica de negoci i/o al model de procés de negoci. No tots aquests canvis són compatibles amb l'antic estat dels processos empresarials. Si hi ha moltes instàncies en directe a la base de dades, fer canvis incompatibles pot causar molts problemes, que sovint ens trobem quan utilitzem jBPM.

En funció de la profunditat dels canvis, podeu actuar de dues maneres:

  1. creeu un nou tipus de procés de negoci per no fer canvis incompatibles a l'antic i utilitzeu-lo en lloc de l'antic quan inicieu instàncies noves. Les còpies antigues continuaran funcionant "com abans";
  2. migrar l'estat persistent dels processos de negoci quan actualitzeu la lògica de negoci.

La primera manera és més senzilla, però té les seves limitacions i inconvenients, per exemple:

  • duplicació de la lògica de negoci en molts models de processos de negoci, augmentant el volum de la lògica de negoci;
  • Sovint es requereix una transició immediata a una nova lògica empresarial (en termes de tasques d'integració, gairebé sempre);
  • el desenvolupador no sap en quin moment es poden suprimir els models obsolets.

A la pràctica fem servir els dos enfocaments, però hem pres diverses decisions per facilitar-nos la vida:

  • A la base de dades, l'estat persistent d'un procés empresarial s'emmagatzema en una forma fàcil de llegir i processar: en una cadena de format JSON. Això permet fer migracions tant dins de l'aplicació com externament. Com a últim recurs, podeu corregir-lo manualment (especialment útil en desenvolupament durant la depuració);
  • la lògica de negoci d'integració no utilitza els noms dels processos de negoci, de manera que en qualsevol moment és possible substituir la implementació d'un dels processos participants per un de nou amb un nom nou (per exemple, "InitialPlayerV2"). La vinculació es produeix mitjançant noms de missatges i senyals;
  • el model de procés té un número de versió, que incrementem si fem canvis incompatibles en aquest model, i aquest número es desa juntament amb l'estat de la instància del procés;
  • l'estat persistent del procés es llegeix primer de la base de dades a un model d'objectes convenient, amb el qual el procediment de migració pot funcionar si el número de versió del model ha canviat;
  • el procediment de migració es col·loca al costat de la lògica empresarial i s'anomena "lazy" per a cada instància del procés empresarial en el moment de la seva restauració des de la base de dades;
  • si necessiteu migrar l'estat de totes les instàncies de procés de manera ràpida i sincrònica, s'utilitzen solucions de migració de bases de dades més clàssiques, però heu de treballar amb JSON.

Necessites un altre marc per als processos de negoci?

Les solucions descrites a l'article ens van permetre simplificar significativament la nostra vida, ampliar el ventall de problemes resolts a nivell de desenvolupament d'aplicacions i fer més atractiva la idea de separar la lògica empresarial en microserveis. Per aconseguir-ho, es va treballar molt, es va crear un marc molt "lleuger" per als processos de negoci, així com components de servei per resoldre els problemes identificats en el context d'una àmplia gamma de problemes d'aplicació. Tenim el desig de compartir aquests resultats i fer que el desenvolupament de components comuns sigui d'accés obert sota una llicència lliure. Això requerirà una mica d'esforç i temps. Comprendre la demanda d'aquestes solucions podria ser un incentiu addicional per a nosaltres. En l'article proposat, es presta molt poca atenció a les capacitats del propi marc, però algunes d'elles són visibles a partir dels exemples presentats. Si publiquem el nostre marc, se li dedicarà un article a part. Mentrestant, us agrairem que deixeu un petit comentari responent a la pregunta:

Només els usuaris registrats poden participar en l'enquesta. Inicia sessiósi us plau.

Necessites un altre marc per als processos de negoci?

  • 18,8%Sí, fa temps que busco una cosa així

  • 12,5%M'interessa saber més sobre la vostra implementació, pot ser útil2

  • 6,2%Utilitzem un dels marcs existents, però estem pensant en substituir-lo1

  • 18,8%Utilitzem un dels frameworks existents, tot està bé3

  • 18,8%gestionem sense marc3

  • 25,0%escriu el teu4

Han votat 16 usuaris. 7 usuaris es van abstenir.

Font: www.habr.com

Afegeix comentari