Integración estilo BPM

Integración estilo BPM

Hola, Habr!

Nuestra empresa se especializa en el desarrollo de soluciones de software de clase ERP, en las que la parte del león está ocupada por sistemas transaccionales con una gran cantidad de lógica comercial y flujo de trabajo a la EDMS. Las versiones modernas de nuestros productos se basan en tecnologías JavaEE, pero también estamos experimentando activamente con microservicios. Una de las áreas más problemáticas de tales soluciones es la integración de varios subsistemas relacionados con dominios adyacentes. Las tareas de integración siempre nos han dado un gran dolor de cabeza, independientemente de los estilos arquitectónicos, las pilas de tecnología y los marcos que usamos, pero recientemente ha habido avances en la solución de tales problemas.

En el artículo presentado a su atención, hablaré sobre la experiencia y la investigación arquitectónica de NPO Krista en el área designada. También consideraremos un ejemplo de una solución simple a un problema de integración desde el punto de vista de un desarrollador de aplicaciones y descubriremos qué se esconde detrás de esta simplicidad.

Descargo de responsabilidad

Las soluciones arquitectónicas y técnicas descritas en el artículo las ofrezco en base a mi experiencia personal en el contexto de tareas específicas. Estas soluciones no pretenden ser universales y pueden no ser óptimas bajo otras condiciones de uso.

¿Qué tiene que ver BPM con esto?

Para responder a esta pregunta, necesitamos profundizar un poco en los detalles de los problemas aplicados de nuestras soluciones. La parte principal de la lógica comercial en nuestro sistema transaccional típico es ingresar datos en la base de datos a través de interfaces de usuario, verificar estos datos de forma manual y automática, pasarlos a través de algún flujo de trabajo, publicarlos en otro sistema/base de datos analítica/archivo, generar informes. Por lo tanto, la función clave del sistema para los clientes es la automatización de sus procesos comerciales internos.

Por comodidad, utilizamos el término "documento" en la comunicación como una abstracción de un conjunto de datos, unidos por una clave común, al que se puede "adjuntar" un flujo de trabajo específico.
Pero, ¿qué pasa con la lógica de integración? Después de todo, la tarea de integración es generada por la arquitectura del sistema, que se "corta" en partes NO a pedido del cliente, sino bajo la influencia de factores completamente diferentes:

  • bajo la influencia de la ley de Conway;
  • como resultado de la reutilización de subsistemas previamente desarrollados para otros productos;
  • según lo decidido por el arquitecto, basado en requisitos no funcionales.

Existe una gran tentación de separar la lógica de integración de la lógica de negocios del flujo de trabajo principal para no contaminar la lógica de negocios con artefactos de integración y evitar que el desarrollador de la aplicación tenga que profundizar en las peculiaridades del paisaje arquitectónico del sistema. Este enfoque tiene una serie de ventajas, pero la práctica muestra su ineficiencia:

  • la resolución de problemas de integración generalmente se reduce a las opciones más simples en forma de llamadas sincrónicas debido a los puntos de extensión limitados en la implementación del flujo de trabajo principal (más información sobre las deficiencias de la integración sincrónica a continuación);
  • los artefactos de integración aún penetran en la lógica comercial principal cuando se requiere retroalimentación de otro subsistema;
  • el desarrollador de la aplicación ignora la integración y puede romperla fácilmente cambiando el flujo de trabajo;
  • el sistema deja de ser un todo único desde el punto de vista del usuario, las "costuras" entre los subsistemas se vuelven notables, aparecen operaciones de usuario redundantes que inician la transferencia de datos de un subsistema a otro.

Otro enfoque es considerar las interacciones de integración como una parte integral de la lógica y el flujo de trabajo centrales del negocio. Para evitar que los requisitos de habilidades de los desarrolladores de aplicaciones se disparen, la creación de nuevas interacciones de integración debe hacerse de manera fácil y natural, con opciones mínimas para elegir una solución. Esto es más difícil de lo que parece: la herramienta debe ser lo suficientemente potente como para proporcionar al usuario la variedad necesaria de opciones para su uso y, al mismo tiempo, no permitir que le disparen en el pie. Hay muchas preguntas que un ingeniero debe responder en el contexto de las tareas de integración, pero en las que un desarrollador de aplicaciones no debe pensar en su trabajo diario: límites de transacción, coherencia, atomicidad, seguridad, escalado, distribución de carga y recursos, enrutamiento, serialización, contextos de propagación y cambio, etc. Es necesario ofrecer a los desarrolladores de aplicaciones plantillas de decisión bastante simples, en las que las respuestas a todas esas preguntas ya están ocultas. Estos patrones deben ser lo suficientemente seguros: la lógica empresarial cambia muy a menudo, lo que aumenta el riesgo de introducir errores, el costo de los errores debe permanecer en un nivel bastante bajo.

Pero aún así, ¿qué tiene que ver BPM con esto? Hay muchas opciones para implementar el flujo de trabajo...
De hecho, otra implementación de procesos comerciales es muy popular en nuestras soluciones: a través de la configuración declarativa del diagrama de transición de estado y la conexión de controladores con lógica comercial a las transiciones. Al mismo tiempo, el estado que determina la posición actual del "documento" en el proceso comercial es un atributo del propio "documento".

Integración estilo BPM
Así es como se ve el proceso al inicio del proyecto

La popularidad de tal implementación se debe a la relativa simplicidad y velocidad de la creación de procesos comerciales lineales. Sin embargo, a medida que los sistemas de software se vuelven más complejos, la parte automatizada del proceso comercial crece y se vuelve más compleja. Existe la necesidad de descomposición, reutilización de partes de procesos, así como procesos de bifurcación para que cada rama se ejecute en paralelo. Bajo tales condiciones, la herramienta se vuelve inconveniente y el diagrama de transición de estado pierde su contenido de información (las interacciones de integración no se reflejan en absoluto en el diagrama).

Integración estilo BPM
Así es como se ve el proceso después de varias iteraciones para aclarar los requisitos

La salida a esta situación fue la integración del motor. jBPM en algunos productos con los procesos de negocio más complejos. A corto plazo, esta solución tuvo cierto éxito: se hizo posible implementar procesos comerciales complejos manteniendo un diagrama bastante informativo y actualizado en la notación. BPMN2.

Integración estilo BPM
Una pequeña parte de un proceso empresarial complejo

A largo plazo, la solución no estuvo a la altura de las expectativas: la alta intensidad de trabajo de crear procesos de negocios a través de herramientas visuales no permitía alcanzar indicadores de productividad aceptables, y la herramienta en sí se convirtió en una de las más desagradables entre los desarrolladores. También hubo quejas sobre la estructura interna del motor, lo que provocó la aparición de muchos "parches" y "muletas".

El principal aspecto positivo de usar jBPM fue darse cuenta de los beneficios y perjuicios de tener su propio estado persistente para una instancia de proceso empresarial. También vimos la posibilidad de usar un enfoque de proceso para implementar protocolos de integración complejos entre diferentes aplicaciones usando interacciones asíncronas a través de señales y mensajes. La presencia de un estado persistente juega un papel crucial en esto.

En base a lo anterior, podemos concluir: El enfoque de procesos en el estilo BPM nos permite resolver una amplia gama de tareas para automatizar procesos comerciales cada vez más complejos, encajar armoniosamente las actividades de integración en estos procesos y conservar la capacidad de mostrar visualmente el proceso implementado en una notación adecuada.

Desventajas de las llamadas síncronas como patrón de integración

La integración síncrona se refiere a la llamada de bloqueo más simple. Un subsistema actúa como el lado del servidor y expone la API con el método deseado. Otro subsistema actúa como lado del cliente y, en el momento adecuado, realiza una llamada con la expectativa de un resultado. Dependiendo de la arquitectura del sistema, los lados del cliente y del servidor pueden estar alojados en la misma aplicación y proceso, o en diferentes. En el segundo caso, debe aplicar alguna implementación de RPC y proporcionar la clasificación de los parámetros y el resultado de la llamada.

Integración estilo BPM

Tal patrón de integración tiene un conjunto bastante grande de inconvenientes, pero se usa mucho en la práctica debido a su simplicidad. La velocidad de implementación cautiva y te hace aplicarla una y otra vez en condiciones de plazos "quemados", escribiendo la solución en deuda técnica. Pero también sucede que los desarrolladores sin experiencia lo usan inconscientemente, simplemente sin darse cuenta de las consecuencias negativas.

Además del aumento más obvio en la conectividad de los subsistemas, existen problemas menos obvios con las transacciones de "difusión" y "extensión". De hecho, si la lógica empresarial realiza algún cambio, las transacciones son indispensables y, a su vez, las transacciones bloquean ciertos recursos de la aplicación afectados por estos cambios. Es decir, hasta que un subsistema espere una respuesta de otro, no podrá completar la transacción y liberar los bloqueos. Esto aumenta significativamente el riesgo de una variedad de efectos:

  • se pierde la capacidad de respuesta del sistema, los usuarios esperan mucho tiempo para obtener respuestas a las solicitudes;
  • el servidor generalmente deja de responder a las solicitudes de los usuarios debido a un grupo de subprocesos desbordados: la mayoría de los subprocesos "se paran" en el bloqueo del recurso ocupado por la transacción;
  • comienzan a aparecer interbloqueos: la probabilidad de que ocurran depende en gran medida de la duración de las transacciones, la cantidad de lógica comercial y los bloqueos involucrados en la transacción;
  • aparecen errores de vencimiento del tiempo de espera de la transacción;
  • el servidor "cae" en OutOfMemory si la tarea requiere procesar y cambiar grandes cantidades de datos, y la presencia de integraciones sincrónicas hace que sea muy difícil dividir el procesamiento en transacciones "más ligeras".

Desde un punto de vista arquitectónico, el uso de llamadas de bloqueo durante la integración conduce a una pérdida de control de calidad de los subsistemas individuales: es imposible garantizar los objetivos de calidad de un subsistema de forma aislada de los objetivos de calidad de otro subsistema. Si los subsistemas son desarrollados por diferentes equipos, esto es un gran problema.

Las cosas se vuelven aún más interesantes si los subsistemas que se integran están en diferentes aplicaciones y es necesario realizar cambios sincrónicos en ambos lados. ¿Cómo hacer que estos cambios sean transaccionales?

Si los cambios se realizan en transacciones separadas, será necesario proporcionar una compensación y un manejo de excepciones robustos, y esto elimina por completo la principal ventaja de las integraciones sincrónicas: la simplicidad.

Las transacciones distribuidas también vienen a la mente, pero no las usamos en nuestras soluciones: es difícil garantizar la confiabilidad.

"Saga" como solución al problema de las transacciones

Con la creciente popularidad de los microservicios, existe una creciente demanda de Patrón de saga.

Este patrón resuelve perfectamente los problemas anteriores de las transacciones largas y también amplía las posibilidades de administrar el estado del sistema desde el lado de la lógica comercial: la compensación después de una transacción fallida puede no hacer retroceder el sistema a su estado original, pero brindar una alternativa. ruta de procesamiento de datos. También le permite no repetir los pasos de procesamiento de datos completados con éxito cuando intenta llevar el proceso a un "buen" final.

Curiosamente, en los sistemas monolíticos, este patrón también es relevante cuando se trata de la integración de subsistemas débilmente acoplados y existen efectos negativos causados ​​por transacciones largas y los bloqueos de recursos correspondientes.

Con respecto a nuestros procesos comerciales en el estilo BPM, resulta muy fácil implementar Sagas: los pasos individuales de Sagas se pueden establecer como actividades dentro del proceso comercial y el estado persistente del proceso comercial determina, entre otras cosas. , el estado interno de las Sagas. Es decir, no necesitamos ningún mecanismo de coordinación adicional. Todo lo que necesita es un intermediario de mensajes con soporte para garantías "al menos una vez" como transporte.

Pero tal solución también tiene su propio "precio":

  • la lógica empresarial se vuelve más compleja: debe calcular la compensación;
  • será necesario abandonar la consistencia total, que puede ser especialmente sensible para los sistemas monolíticos;
  • la arquitectura se vuelve un poco más complicada, hay una necesidad adicional de un intermediario de mensajes;
  • se requerirán herramientas adicionales de monitoreo y administración (aunque en general esto es incluso bueno: la calidad del servicio del sistema aumentará).

Para sistemas monolíticos, la justificación para usar "Sags" no es tan obvia. Para los microservicios y otras SOA, donde lo más probable es que ya haya un intermediario y se sacrificó la consistencia total al comienzo del proyecto, los beneficios de usar este patrón pueden superar significativamente las desventajas, especialmente si hay una API conveniente al principio. nivel de lógica de negocios.

Encapsulación de lógica de negocio en microservicios

Cuando comenzamos a experimentar con microservicios, surgió una pregunta razonable: ¿dónde ubicar la lógica comercial del dominio en relación con el servicio que proporciona persistencia de datos de dominio?

Al observar la arquitectura de varios BPMS, puede parecer razonable separar la lógica comercial de la persistencia: cree una capa de plataforma y microservicios independientes del dominio que formen el entorno y el contenedor para ejecutar la lógica comercial del dominio, y organice la persistencia de los datos del dominio como una capa separada. capa de microservicios muy simples y ligeros. Los procesos de negocio en este caso orquestan los servicios de la capa de persistencia.

Integración estilo BPM

Este enfoque tiene una gran ventaja: puede aumentar la funcionalidad de la plataforma tanto como desee, y solo la capa correspondiente de microservicios de la plataforma "engordará" a partir de esto. Los procesos comerciales de cualquier dominio tienen inmediatamente la oportunidad de utilizar la nueva funcionalidad de la plataforma tan pronto como se actualice.

Un estudio más detallado reveló deficiencias significativas de este enfoque:

  • un servicio de plataforma que ejecuta la lógica comercial de muchos dominios a la vez conlleva grandes riesgos como único punto de falla. Los cambios frecuentes en la lógica empresarial aumentan el riesgo de errores que provocan fallas en todo el sistema;
  • problemas de rendimiento: la lógica empresarial trabaja con sus datos a través de una interfaz estrecha y lenta:
    • los datos volverán a ordenarse y bombearse a través de la pila de red;
    • el servicio de dominio a menudo devolverá más datos de los que requiere la lógica comercial para el procesamiento, debido a capacidades insuficientes de parametrización de consultas en el nivel de la API externa del servicio;
    • varias piezas independientes de la lógica comercial pueden volver a solicitar repetidamente los mismos datos para su procesamiento (puede mitigar este problema agregando beans de sesión que almacenan datos en caché, pero esto complica aún más la arquitectura y crea problemas de actualización de datos e invalidación de caché);
  • problemas transaccionales:
    • los procesos comerciales con estado persistente almacenado por el servicio de la plataforma son inconsistentes con los datos del dominio y no hay formas fáciles de resolver este problema;
    • mover el bloqueo de los datos del dominio fuera de la transacción: si la lógica comercial del dominio necesita realizar cambios, después de verificar primero la exactitud de los datos reales, es necesario excluir la posibilidad de un cambio competitivo en los datos procesados. El bloqueo externo de datos puede ayudar a resolver el problema, pero dicha solución conlleva riesgos adicionales y reduce la confiabilidad general del sistema;
  • Complicaciones adicionales al actualizar: en algunos casos, debe actualizar el servicio de persistencia y la lógica comercial de forma síncrona o en secuencia estricta.

Al final, tuve que volver a lo básico: encapsular los datos del dominio y la lógica comercial del dominio en un microservicio. Este enfoque simplifica la percepción del microservicio como un componente integral del sistema y no da lugar a los problemas anteriores. Esto tampoco es gratis:

  • Se requiere la estandarización de la API para la interacción con la lógica comercial (en particular, para proporcionar actividades de usuario como parte de los procesos comerciales) y los servicios de la plataforma API; se requiere una atención más cuidadosa a los cambios de API, compatibilidad hacia adelante y hacia atrás;
  • se requiere agregar bibliotecas de tiempo de ejecución adicionales para garantizar el funcionamiento de la lógica de negocios como parte de cada microservicio, y esto da lugar a nuevos requisitos para dichas bibliotecas: ligereza y un mínimo de dependencias transitivas;
  • Los desarrolladores de lógica empresarial deben realizar un seguimiento de las versiones de la biblioteca: si un microservicio no se finalizó durante mucho tiempo, lo más probable es que contenga una versión desactualizada de las bibliotecas. Esto puede ser un obstáculo inesperado para agregar una nueva función y puede requerir que la lógica comercial anterior de dicho servicio se migre a nuevas versiones de las bibliotecas si hubo cambios incompatibles entre las versiones.

Integración estilo BPM

Una capa de servicios de plataforma también está presente en dicha arquitectura, pero esta capa ya no forma un contenedor para ejecutar la lógica comercial del dominio, sino solo su entorno, proporcionando funciones auxiliares de "plataforma". Esta capa es necesaria no solo para mantener la ligereza de los microservicios de dominio, sino también para centralizar la gestión.

Por ejemplo, las actividades de los usuarios en los procesos comerciales generan tareas. Sin embargo, cuando se trabaja con tareas, el usuario debe ver las tareas de todos los dominios en la lista general, lo que significa que debe haber un servicio de plataforma de registro de tareas adecuado, sin lógica comercial de dominio. Mantener la encapsulación de la lógica empresarial en este contexto es bastante problemático, y este es otro compromiso de esta arquitectura.

Integración de procesos de negocio a través de los ojos de un desarrollador de aplicaciones

Como ya se mencionó anteriormente, el desarrollador de aplicaciones debe abstraerse de las características técnicas y de ingeniería de la implementación de la interacción de varias aplicaciones para poder contar con una buena productividad de desarrollo.

Intentemos resolver un problema de integración bastante difícil, especialmente inventado para el artículo. Esta será una tarea de "juego" que involucrará tres aplicaciones, donde cada una de ellas define algún nombre de dominio: "app1", "app2", "app3".

Dentro de cada aplicación, se lanzan procesos comerciales que comienzan a "jugar a la pelota" a través del bus de integración. Los mensajes llamados "Pelota" actuarán como la pelota.

Reglas del juego:

  • el primer jugador es el iniciador. Invita a otros jugadores al juego, inicia el juego y puede finalizarlo en cualquier momento;
  • otros jugadores declaran su participación en el juego, "se familiarizan" entre sí y con el primer jugador;
  • después de recibir la pelota, el jugador elige a otro jugador participante y le pasa la pelota. Se cuenta el número total de pases;
  • cada jugador tiene "energía", que disminuye con cada pase de balón de ese jugador. Cuando se agota la energía, el jugador es eliminado del juego, anunciando su retiro;
  • si el jugador se queda solo, inmediatamente declara su salida;
  • cuando todos los jugadores son eliminados, el primer jugador declara el final del juego. Si dejó el juego antes, queda seguir el juego para completarlo.

Para resolver este problema, usaré nuestro DSL para procesos comerciales, que le permite describir la lógica en Kotlin de forma compacta, con un mínimo de repetitivo.

En la aplicación app1, funcionará el proceso comercial del primer jugador (también es el iniciador del juego):

clase JugadorInicial

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

Además de ejecutar la lógica empresarial, el código anterior puede producir un modelo de objeto de un proceso empresarial que se puede visualizar como un diagrama. Todavía no hemos implementado el visualizador, por lo que tuvimos que dedicar un tiempo a dibujar (aquí simplifiqué un poco la notación BPMN con respecto al uso de puertas para mejorar la consistencia del diagrama con el código anterior):

Integración estilo BPM

app2 incluirá el proceso comercial de otro jugador:

clase 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ón estilo BPM

En la aplicación app3, haremos que el jugador tenga un comportamiento ligeramente diferente: en lugar de elegir aleatoriamente al siguiente jugador, actuará de acuerdo con el algoritmo de todos contra todos:

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

Por lo demás, el comportamiento del jugador no difiere del anterior, por lo que el diagrama no cambia.

Ahora necesitamos una prueba para ejecutarlo todo. Daré solo el código de la prueba en sí, para no saturar el artículo con un modelo estándar (de hecho, utilicé el entorno de prueba creado anteriormente para probar la integración de otros procesos comerciales):

juego de prueba ()

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

Ejecute la prueba, mire el registro:

salida de 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 todo esto se pueden sacar varias conclusiones importantes:

  • si las herramientas necesarias están disponibles, los desarrolladores de aplicaciones pueden crear interacciones de integración entre aplicaciones sin romper con la lógica empresarial;
  • la complejidad (complejidad) de una tarea de integración que requiere competencias de ingeniería puede ocultarse dentro del marco si se establece inicialmente en la arquitectura del marco. La dificultad de la tarea (dificultad) no se puede ocultar, por lo que la solución a una tarea difícil en el código se verá en consecuencia;
  • al desarrollar la lógica de integración, es necesario tener en cuenta la consistencia eventual y la falta de linealización del cambio de estado de todos los participantes de la integración. Esto nos obliga a complicar la lógica para hacerla insensible al orden en que ocurren los eventos externos. En nuestro ejemplo, el jugador se ve obligado a participar en el juego después de anunciar su salida del juego: otros jugadores continuarán pasándole la pelota hasta que la información sobre su salida llegue y sea procesada por todos los participantes. Esta lógica no se deriva de las reglas del juego y es una solución de compromiso en el marco de la arquitectura elegida.

A continuación, hablemos de las diversas sutilezas de nuestra solución, compromisos y otros puntos.

Todos los mensajes en una cola

Todas las aplicaciones integradas funcionan con un bus de integración, que se presenta como un intermediario externo, un BPMQueue para mensajes y un tema BPMTopic para señales (eventos). Pasar todos los mensajes a través de una sola cola es en sí mismo un compromiso. En el nivel de lógica empresarial, ahora puede introducir tantos tipos de mensajes nuevos como desee sin realizar cambios en la estructura del sistema. Esta es una simplificación significativa, pero conlleva ciertos riesgos que, en el contexto de nuestras tareas típicas, no nos parecieron tan significativos.

Integración estilo BPM

Sin embargo, aquí hay una sutileza: cada aplicación filtra "sus" mensajes de la cola en la entrada, por el nombre de su dominio. Además, el dominio se puede especificar en las señales, si necesita limitar el "alcance" de la señal a una sola aplicación. Esto debería aumentar el ancho de banda del bus, pero la lógica comercial ahora debe operar con nombres de dominio: obligatorio para direccionar mensajes, deseable para señales.

Garantía de la fiabilidad del bus de integración

La confiabilidad se compone de varias cosas:

  • El intermediario de mensajes elegido es un componente crítico de la arquitectura y un único punto de falla: debe ser lo suficientemente tolerante a fallas. Debe usar solo implementaciones probadas con un buen soporte y una gran comunidad;
  • es necesario asegurar una alta disponibilidad del intermediario de mensajes, para lo cual debe estar separado físicamente de las aplicaciones integradas (la alta disponibilidad de aplicaciones con lógica de negocios aplicada es mucho más difícil y costosa de proporcionar);
  • el corredor está obligado a proporcionar "al menos una vez" garantías de entrega. Este es un requisito obligatorio para el funcionamiento fiable del bus de integración. No hay necesidad de garantías de nivel "exactamente una vez": los procesos comerciales generalmente no son sensibles a la llegada repetida de mensajes o eventos, y en tareas especiales donde esto es importante, es más fácil agregar controles adicionales a la lógica comercial que usar constantemente garantías más bien "caras";
  • el envío de mensajes y señales debe estar involucrado en una transacción común con un cambio en el estado de los procesos comerciales y los datos del dominio. La opción preferida sería usar el patrón Bandeja de salida transaccional, pero requerirá una tabla adicional en la base de datos y un relé. En las aplicaciones JEE, esto se puede simplificar utilizando un administrador JTA local, pero la conexión con el intermediario seleccionado debe poder funcionar en modo XA;
  • los controladores de mensajes y eventos entrantes también deben trabajar con la transacción de cambiar el estado del proceso comercial: si dicha transacción se revierte, entonces también se debe cancelar la recepción del mensaje;
  • los mensajes que no se pudieron entregar debido a errores deben almacenarse en un almacén separado DLQ. (Cola de mensajes fallidos). Para hacer esto, creamos un microservicio de plataforma independiente que almacena dichos mensajes en su almacenamiento, los indexa por atributos (para agrupar y buscar rápidamente) y expone la API para ver, reenviar a la dirección de destino y eliminar mensajes. Los administradores del sistema pueden trabajar con este servicio a través de su interfaz web;
  • en la configuración del intermediario, debe ajustar la cantidad de reintentos de entrega y demoras entre las entregas para reducir la probabilidad de que los mensajes ingresen al DLQ (es casi imposible calcular los parámetros óptimos, pero puede actuar empíricamente y ajustarlos durante operación);
  • el almacén DLQ debe monitorearse continuamente y el sistema de monitoreo debe notificar a los administradores del sistema para que puedan responder lo más rápido posible cuando se produzcan mensajes no entregados. Esto reducirá la "zona de daño" de una falla o error de lógica comercial;
  • el bus de integración debe ser insensible a la ausencia temporal de aplicaciones: las suscripciones de temas deben ser duraderas y el nombre de dominio de la aplicación debe ser único para que nadie más intente procesar su mensaje de la cola durante la ausencia de la aplicación.

Garantizar la seguridad de subprocesos de la lógica empresarial

Una misma instancia de un proceso de negocio puede recibir varios mensajes y eventos a la vez, cuyo procesamiento comenzará en paralelo. Al mismo tiempo, para un desarrollador de aplicaciones, todo debe ser simple y seguro para subprocesos.

La lógica empresarial del proceso procesa cada evento externo que afecta a este proceso empresarial individualmente. Estos eventos pueden ser:

  • lanzamiento de una instancia de proceso de negocio;
  • una acción del usuario relacionada con una actividad dentro de un proceso comercial;
  • recepción de un mensaje o señal a la que está suscrita una instancia de proceso de negocio;
  • expiración del temporizador establecido por la instancia de proceso de negocio;
  • acción de control a través de la API (por ejemplo, cancelación del proceso).

Cada uno de estos eventos puede cambiar el estado de una instancia de proceso empresarial: algunas actividades pueden finalizar y otras comenzar, los valores de las propiedades persistentes pueden cambiar. El cierre de cualquier actividad puede resultar en la activación de una o más de las siguientes actividades. Éstos, a su vez, pueden dejar de esperar otros eventos o, si no necesitan ningún dato adicional, pueden completarse en la misma transacción. Antes de cerrar la transacción, el nuevo estado del proceso comercial se almacena en la base de datos, donde esperará el próximo evento externo.

Los datos de procesos comerciales persistentes almacenados en una base de datos relacional son un punto de sincronización de procesamiento muy conveniente cuando se usa SELECCIONAR PARA ACTUALIZAR. Si una transacción logró obtener el estado del proceso comercial desde la base para cambiarlo, entonces ninguna otra transacción en paralelo podrá obtener el mismo estado para otro cambio, y después de completar la primera transacción, la segunda es garantizado para recibir el estado ya cambiado.

Usando bloqueos pesimistas en el lado DBMS, cumplimos con todos los requisitos necesarios ACID, y también conservar la capacidad de escalar la aplicación con lógica de negocios al aumentar la cantidad de instancias en ejecución.

Sin embargo, los bloqueos pesimistas nos amenazan con interbloqueos, lo que significa que SELECCIONAR PARA ACTUALIZAR aún debe limitarse a un tiempo de espera razonable en caso de interbloqueos en algunos casos graves en la lógica empresarial.

Otro problema es la sincronización del inicio del proceso comercial. Si bien no existe una instancia de proceso empresarial, tampoco existe un estado en la base de datos, por lo que el método descrito no funcionará. Si desea garantizar la unicidad de una instancia de proceso empresarial en un ámbito particular, necesita algún tipo de objeto de sincronización asociado con la clase de proceso y el ámbito correspondiente. Para resolver este problema, utilizamos un mecanismo de bloqueo diferente que nos permite bloquear un recurso arbitrario especificado por una clave en formato URI a través de un servicio externo.

En nuestros ejemplos, el proceso comercial InitialPlayer contiene una declaración

uniqueConstraint = UniqueConstraints.singleton

Por lo tanto, el registro contiene mensajes sobre cómo tomar y liberar el bloqueo de la llave correspondiente. No hay mensajes de este tipo para otros procesos de negocio: no se ha establecido uniqueConstraint.

Problemas de procesos de negocio con estado persistente

A veces, tener un estado persistente no solo ayuda, sino que también dificulta el desarrollo.
Los problemas comienzan cuando necesita realizar cambios en la lógica comercial y/o el modelo de proceso comercial. No se encuentra que ningún cambio de este tipo sea compatible con el estado anterior de los procesos comerciales. Si hay muchas instancias "en vivo" en la base de datos, entonces hacer cambios incompatibles puede causar muchos problemas, que a menudo encontramos cuando usamos jBPM.

Dependiendo de la profundidad del cambio, puede actuar de dos maneras:

  1. cree un nuevo tipo de proceso comercial para no realizar cambios incompatibles con el anterior y utilícelo en lugar del anterior al iniciar nuevas instancias. Las instancias antiguas seguirán funcionando "a la antigua";
  2. migrar el estado persistente de los procesos comerciales al actualizar la lógica comercial.

La primera forma es más sencilla, pero tiene sus limitaciones y desventajas, por ejemplo:

  • duplicación de la lógica comercial en muchos modelos de procesos comerciales, un aumento en el volumen de la lógica comercial;
  • a menudo se requiere una transición instantánea a una nueva lógica de negocios (casi siempre en términos de tareas de integración);
  • el desarrollador no sabe en qué momento es posible eliminar modelos obsoletos.

En la práctica, usamos ambos enfoques, pero hemos tomado una serie de decisiones para simplificar nuestras vidas:

  • en la base de datos, el estado persistente del proceso comercial se almacena en una forma fácil de leer y procesar: en una cadena de formato JSON. Esto le permite realizar migraciones tanto dentro como fuera de la aplicación. En casos extremos, también puede modificarlo con identificadores (especialmente útil en el desarrollo durante la depuración);
  • la lógica de negocios de integración no utiliza los nombres de los procesos de negocios, por lo que en cualquier momento es posible reemplazar la implementación de uno de los procesos participantes por uno nuevo, con un nuevo nombre (por ejemplo, "InitialPlayerV2"). El enlace se produce a través de los nombres de mensajes y señales;
  • el modelo de proceso tiene un número de versión, que incrementamos si hacemos cambios incompatibles con este modelo, y este número se almacena junto con el estado de la instancia de proceso;
  • el estado persistente del proceso se lee desde la base primero en un modelo de objeto conveniente con el que puede trabajar el procedimiento de migración si el número de versión del modelo ha cambiado;
  • el procedimiento de migración se coloca junto a la lógica empresarial y se denomina "perezoso" para cada instancia del proceso empresarial en el momento de su restauración desde la base de datos;
  • si necesita migrar el estado de todas las instancias de proceso de forma rápida y sincrónica, se utilizan soluciones de migración de bases de datos más clásicas, pero allí debe trabajar con JSON.

¿Necesito otro marco para los procesos de negocio?

Las soluciones descritas en el artículo nos permitieron simplificar significativamente nuestras vidas, ampliar la gama de problemas resueltos a nivel de desarrollo de aplicaciones y hacer más atractiva la idea de separar la lógica empresarial en microservicios. Para esto, se ha trabajado mucho, se ha creado un marco muy "ligero" para los procesos comerciales, así como componentes de servicio para resolver los problemas identificados en el contexto de una amplia gama de tareas aplicadas. Tenemos el deseo de compartir estos resultados, para llevar el desarrollo de componentes comunes al acceso abierto bajo una licencia gratuita. Esto requerirá algo de esfuerzo y tiempo. Comprender la demanda de tales soluciones podría ser un incentivo adicional para nosotros. En el artículo propuesto, se presta muy poca atención a las capacidades del marco en sí, pero algunas de ellas son visibles a partir de los ejemplos presentados. Sin embargo, si publicamos nuestro marco, se le dedicará un artículo separado. Mientras tanto, le agradeceremos que deje un pequeño comentario respondiendo a la pregunta:

Solo los usuarios registrados pueden participar en la encuesta. Registrarsepor favor

¿Necesito otro marco para los procesos de negocio?

  • 18,8%Sí, he estado buscando algo así durante mucho tiempo.

  • 12,5%es interesante saber mas sobre su implementacion, puede ser util2

  • 6,2%usamos uno de los marcos existentes, pero estamos pensando en reemplazarlo1

  • 18,8%usamos uno de los marcos existentes, todo encaja3

  • 18,8%afrontamiento sin marco3

  • 25,0%escribe el tuyo 4

16 usuarios votaron. 7 usuarios se abstuvieron.

Fuente: habr.com

Añadir un comentario