Integración al estilo BPM

Integración al estilo BPM

Hola, Habr!

Nuestra empresa se especializa en el desarrollo de soluciones de software de clase ERP, la cual incluye en su mayoría sistemas transaccionales con una gran cantidad de lógica empresarial y flujo documental similar a un sistema de gestión de documentos. Las versiones modernas de nuestros productos se basan en tecnologías JavaEE, pero también estamos experimentando activamente con microservicios. Uno de los aspectos más problemáticos de tales soluciones es la integración de diversos subsistemas relacionados con dominios adyacentes. Las tareas de integración siempre nos han causado grandes dolores de cabeza, independientemente de los estilos arquitectónicos, pilas tecnológicas y marcos que utilizamos, sin embargo, recientemente hemos visto un progreso en la resolución de tales desafíos.

En el artículo que se presenta a su atención, hablaré sobre la experiencia y las exploraciones arquitectónicas del NPO 'Krista' en esta área. También analizaremos un ejemplo de una solución simple a un problema de integración desde la perspectiva de un desarrollador de aplicaciones y descubriremos qué se esconde detrás de esa simplicidad.

Descargo de responsabilidad

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

¿Qué tiene que ver BPM?

Para responder a esta pregunta, es necesario profundizar un poco en la especificidad de las tareas aplicativas de nuestras soluciones. La mayor parte de la lógica empresarial en nuestro sistema transaccional típico consiste en la introducción de datos en la base de datos a través de interfaces de usuario, la verificación manual y automatizada de estos datos, su procesamiento a través de un flujo de trabajo, la publicación en otro sistema / base de datos analítica / archivo, y la generación de informes. Así, la función clave del sistema para los clientes es la automatización de sus procesos de negocio internos.

Para mayor comodidad, usamos en nuestra comunicación el término 'documento' como una abstracción de un conjunto de datos unidos por una clave común, a la que se puede 'vincular' un flujo de trabajo específico.
Pero, ¿qué pasa con la lógica de integración? La tarea de integrar surge de la arquitectura del sistema, que está 'dividida' en partes NO por la demanda del cliente, sino bajo la influencia de otros factores:

  • bajo la ley de Conway;
  • como resultado de la reutilización de subsistemas previamente desarrollados para otros productos;
  • por decisión del arquitecto, basándose en los requisitos no funcionales.

Hay una gran tentación de separar la lógica de integración de la lógica empresarial del flujo de trabajo principal, para no contaminar la lógica empresarial con artefactos de integración y liberar al desarrollador de aplicaciones de la necesidad de comprender las particularidades del paisaje arquitectónico del sistema. Este enfoque tiene varias ventajas, sin embargo, la práctica demuestra su ineficacia:

  • la solución de problemas de integración generalmente se reduce a las opciones más simples en forma de llamadas síncronas debido a la limitación de puntos de expansión en la implementación del flujo de trabajo principal (sobre las desventajas de la integración síncrona, se hablará más adelante);
  • los artefactos de integración aún se infiltran en la lógica empresarial principal cuando se requiere retroalimentación de otro subsistema;
  • el desarrollador de aplicaciones ignora la integración y puede romperla fácilmente al cambiar el flujo de trabajo;
  • el sistema deja de ser una entidad única desde el punto de vista del usuario, se hacen visibles las "costuras" entre los subsistemas, y surgen operaciones de usuario redundantes que inician la transferencia de datos de un subsistema a otro.

Un enfoque diferente es considerar las interacciones de integración como una parte esencial de la lógica empresarial principal y del flujo de trabajo. Para que los requisitos de cualificación de los desarrolladores de aplicaciones no se disparen, la creación de nuevas interacciones de integración debe realizarse de manera fácil y fluida, con las mínimas opciones posibles para decidir el enfoque. Esto es más difícil de lo que parece: la herramienta debe ser lo suficientemente potente para proporcionar al usuario la variedad necesaria de opciones de aplicación, y al mismo tiempo, no permitir que se auto-sabotee. Hay muchas preguntas que un ingeniero debe responder en el contexto de tareas de integración, pero que un desarrollador de aplicaciones no debería considerar en su trabajo diario: los límites de las transacciones, la consistencia, la atomicidad, la seguridad, la escalabilidad, la distribución de cargas y recursos, el enrutamiento, el marshalling, la difusión y el cambio de contextos, etc. Deben ofrecerse a los desarrolladores de aplicaciones plantillas de soluciones lo suficientemente simples, que contengan ya respuestas a todas estas preguntas. Estas plantillas deben ser lo suficientemente seguras: la lógica empresarial cambia muy a menudo, lo que aumenta el riesgo de cometer errores, y el costo de los errores debe permanecer en un nivel suficientemente bajo.

Pero, ¿qué tiene que ver BPM con esto? Existen muchas alternativas para implementar el flujo de trabajo...
De hecho, en nuestras soluciones es muy popular otra implementación de procesos empresariales: a través de la definición declarativa de diagramas de transiciones de estado y la conexión de controladores con lógica empresarial en las transiciones. En este caso, el estado que define la posición actual del "documento" en el proceso empresarial es un atributo del propio "documento".

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

La popularidad de esta implementación se debe a la relativa simplicidad y rapidez de creación de procesos empresariales lineales. Sin embargo, a medida que los sistemas de software se vuelven cada vez más complejos, la parte automatizada del proceso empresarial se expande y complica. Surge la necesidad de descomposición, reutilización de partes de los procesos, así como de ramificación de procesos, para que cada rama se ejecute en paralelo. En estas condiciones, la herramienta se vuelve incómoda y el diagrama de transiciones de estados pierde su informatividad (las interacciones de integración no se reflejan en absoluto en el diagrama).

Integración al estilo BPM
Así es como se ve el proceso tras varias iteraciones de aclaración de requisitos.

La salida de esta situación fue la integración del motor jBPM en algunos productos con los procesos empresariales más complejos. A corto plazo, esta solución tuvo cierto éxito: surgió la posibilidad de implementar procesos empresariales complejos manteniendo un diagrama bastante informativo y actual en la notación BPMN2.

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

A largo plazo, la solución no cumplió con las expectativas: la alta laboriosidad de crear procesos empresariales a través de herramientas visuales no permitió alcanzar indicadores aceptables de productividad, y la propia herramienta se convirtió en una de las menos apreciadas entre los desarrolladores. También hubo quejas sobre la estructura interna del motor, que llevaron a la aparición de numerosos 'parches' y 'mecanismos temporales'.

El principal aspecto positivo de la aplicación de jBPM fue la conciencia de los beneficios y daños de tener un estado persistente propio en una instancia de proceso empresarial. También vimos la posibilidad de aplicar un enfoque de proceso para implementar complejos protocolos de integración entre diversas aplicaciones utilizando interacciones asíncronas a través de señales y mensajes. La existencia de un estado persistente juega un papel crucial en esto.

Con base en lo dicho, se puede concluir que el enfoque de procesos en estilo BPM nos permite abordar una amplia gama de tareas de automatización de procesos empresariales cada vez más complejos, integrar actividades de integración en estos procesos de manera armoniosa y mantener la posibilidad de visualizar el proceso implementado en una notación adecuada.

Desventajas de las llamadas sincrónicas como patrón de integración

La integración sincrónica se entiende como la llamada bloqueante más simple. Un subsistema actúa como la parte del servidor y ofrece una API con el método deseado. El otro subsistema actúa como cliente y en el momento necesario realiza la llamada esperando el resultado. Dependiendo de la arquitectura del sistema, las partes cliente y servidor pueden estar ubicadas en una misma aplicación y proceso, o en diferentes. En el segundo caso, es necesario aplicar alguna implementación de RPC y asegurar el marshalling de los parámetros y el resultado de la llamada.

Integración al estilo BPM

Este patrón de integración tiene un conjunto bastante grande de desventajas, pero se utiliza ampliamente en la práctica por su simplicidad. La velocidad de implementación es atractiva y lleva a su uso repetido en condiciones de «plazos ajustados», acumulando soluciones en la deuda técnica. Sin embargo, también ocurre que desarrolladores inexperimentados lo aplican de manera inconsciente, simplemente sin darse cuenta de las consecuencias negativas.

Además del aumento más obvio en la cohesión de subsistemas, hay problemas menos evidentes con la «fragmentación» y la «prolongación» de las transacciones. De hecho, si la lógica de negocio realiza cambios, no se puede prescindir de transacciones, y las transacciones, a su vez, bloquean ciertos recursos de la aplicación afectados por estos cambios. Es decir, mientras un subsistema no reciba respuesta de otro, no podrá completar la transacción y liberar los bloqueos. Esto aumenta significativamente el riesgo de diversos efectos:

  • la capacidad de respuesta del sistema se pierde, los usuarios esperan mucho tiempo por respuestas a sus solicitudes;
  • el servidor deja de responder a las solicitudes de los usuarios debido a que el pool de hilos está saturado: la mayoría de los hilos están «detenidos» esperando en el bloqueo de un recurso ocupado por la transacción;
  • empiezan a aparecer deadlocks: la probabilidad de su aparición depende en gran medida de la duración de las transacciones, la cantidad de lógica de negocio involucrada en la transacción y los bloqueos;
  • aparecen errores de tiempo de espera de la transacción;
  • el servidor se «cae» por OutOfMemory si la tarea requiere procesar y modificar grandes volúmenes de datos, y la presencia de integraciones sincrónicas dificulta considerablemente la fragmentación del procesamiento en transacciones más «ligeras».

Desde el punto de vista arquitectónico, el uso de llamadas bloqueantes durante la integración conduce a una pérdida de control sobre la calidad de los subsistemas individuales: no se pueden garantizar los indicadores de calidad de un subsistema de manera independiente a los indicadores de calidad de otro subsistema. Si los subsistemas son desarrollados por equipos diferentes, esto representa un gran problema.

La situación se vuelve aún más interesante si los subsistemas integrados están en diferentes aplicaciones y es necesario realizar cambios síncronos desde ambos lados. ¿Cómo garantizar la atomicidad de esos cambios?

Si los cambios se realizan en transacciones separadas, será necesario garantizar un manejo confiable de excepciones y compensaciones, lo que anula completamente la principal ventaja de las integraciones síncronas: la simplicidad.

También vienen a la mente las transacciones distribuidas, pero no las utilizamos en nuestras soluciones: es complicado garantizar la fiabilidad.

La ‘Saga’ como solución al problema de las transacciones

Con el aumento de la popularidad de los microservicios, se vuelve cada vez más demandado Patrón Saga.

Este patrón soluciona muy bien los problemas mencionados de las transacciones prolongadas, además de ampliar las capacidades de gestión del estado del sistema desde la lógica de negocio: la compensación tras una transacción fallida puede no restaurar el sistema a su estado inicial, sino proporcionar una ruta alternativa para el procesamiento de datos. Esto también permite no repetir los pasos de procesamiento de datos exitosos en intentos posteriores para llevar el proceso a un final ‘bueno’.

Curiosamente, en sistemas monolíticos, este patrón también es relevante si se trata de la integración de subsistemas débilmente acoplados y se observan efectos negativos causados por transacciones prolongadas y las correspondientes bloqueos de recursos.

En relación con nuestros procesos de negocio estilo BPM, implementar ‘Sagas’ resulta muy sencillo: los pasos individuales de una ‘Saga’ pueden definirse como actividades dentro del proceso de negocio, y el estado persistente del proceso de negocio define también el estado interno de la ‘Saga’. Es decir, no necesitamos ningún mecanismo de coordinación adicional. Solo se requerirá un intermediario de mensajes con garantías de ‘al menos una vez’ como transporte.

Pero este tipo de solución también tiene su ‘costo’:

  • la lógica empresarial se está volviendo más compleja: es necesario trabajar en compensaciones;
  • será necesario renunciar a la consistencia total, lo cual puede ser especialmente sensible para los sistemas monolíticos;
  • la arquitectura se complica un poco, apareciendo una necesidad adicional de un intermediario de mensajes;
  • se requerirán herramientas adicionales de monitoreo y administración (aunque en general esto es positivo: la calidad del servicio del sistema mejorará).

Para los sistemas monolíticos, la justificación de utilizar "Saga" no es tan obvia. Para microservicios y otros SOA, donde probablemente ya hay un intermediario y la consistencia total fue sacrificada desde el inicio del proyecto, el beneficio de usar este patrón puede superar significativamente las desventajas, especialmente si hay una API conveniente a nivel de lógica empresarial.

Encapsulación de la lógica empresarial en microservicios

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

Al observar la arquitectura de varios BPMS, puede parecer razonable separar la lógica empresarial de la persistencia: crear una capa de microservicios de plataforma y dominio independiente, que forme un entorno y contenedor para la ejecución de la lógica empresarial del dominio, mientras se diseña la persistencia de los datos del dominio como una capa separada de microservicios muy simples y ligeros. En este caso, los procesos de negocio orquestan los servicios de la capa de persistencia.

Integración al estilo BPM

Este enfoque tiene una gran ventaja: se puede aumentar la funcionalidad de la plataforma tanto como se desee, y solo la capa de microservicios de plataforma correspondiente se verá afectada. Los procesos de negocio de cualquier dominio obtienen inmediatamente la posibilidad de usar la nueva funcionalidad de la plataforma, tan pronto como sea actualizada.

Un análisis más detallado reveló desventajas significativas de este enfoque:

  • el servicio de plataforma que ejecuta la lógica de negocio de varios dominios presenta grandes riesgos como un único punto de falla. Los cambios frecuentes en la lógica empresarial aumentan el riesgo de errores que pueden causar fallos que se propagan a 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 se procesarán innecesariamente y pasarán por la pila de red.
    • El servicio de dominio a menudo devolverá más datos de los que la lógica de negocio necesita para el procesamiento, debido a capacidades insuficientes de parametrización de solicitudes a nivel de API externa.
    • Varias partes independientes de la lógica de negocio pueden volver a solicitar los mismos datos para el procesamiento (esto se puede mitigar agregando componentes de sesión que almacenan en caché los datos, pero eso complica aún más la arquitectura y crea problemas de actualidad de los datos y de invalidación de la caché).
  • Problemas de transaccionalidad:
    • Los procesos de negocio con estado persistente, cuyo almacenamiento es gestionado por el servicio de plataforma, se desincronizan con los datos del dominio, y no se prevén soluciones simples para este problema.
    • Desplazamiento del bloqueo de datos de dominio fuera de la transacción: si la lógica de negocio del dominio necesita realizar modificaciones tras verificar la validez de los datos actuales, es necesario excluir la posibilidad de cambios competitivos en los datos procesados. Un bloqueo externo de los datos puede ayudar a resolver el problema, pero esta solución conlleva riesgos adicionales y disminuye la fiabilidad general del sistema.
  • Complejidades adicionales durante la actualización: en algunos casos, es necesario actualizar el servicio de persistencia y la lógica de negocio de forma sincrónica o en un orden estricto.

Al final, fue necesario volver a los fundamentos: encapsular los datos del dominio y la lógica de negocio del dominio en un único microservicio. Este enfoque simplifica la percepción del microservicio como un componente integral dentro del sistema y no genera los problemas mencionados anteriormente. Esto también conlleva costos:

  • Se requiere la estandarización de la API para interactuar con la lógica de negocio (en particular, para garantizar actividades de usuario dentro de los procesos de negocio) y de las APIs de los servicios de plataforma; se requiere una atención más cuidadosa a los cambios en la API, así como a la compatibilidad hacia adelante y hacia atrás.
  • Se requiere la adición de bibliotecas adicionales en tiempo de ejecución para asegurar el funcionamiento de la lógica de negocio dentro de cada uno de estos microservicios, y esto genera nuevos requisitos para dichas bibliotecas: ligereza y un mínimo de dependencias transitivas.
  • Los desarrolladores de la lógica empresarial deben estar atentos a las versiones de las bibliotecas: si algún microservicio no ha sido actualizado durante mucho tiempo, lo más probable es que contenga una versión obsoleta de bibliotecas. Esto puede convertirse en un obstáculo inesperado para agregar una nueva función y puede requerir la migración de la lógica empresarial antigua de dicho servicio a nuevas versiones de librerías, si entre versiones hubo cambios incompatibles.

Integración al estilo BPM

El capa de servicios de plataforma en esta arquitectura también está presente, pero esta capa no forma un contenedor para la ejecución de la lógica empresarial del dominio, sino que simplemente proporciona un entorno, ofreciendo funciones "plataforma" auxiliares. Esta capa no solo es necesaria para mantener la ligereza de los microservicios del dominio, sino también para centralizar la gestión.

Por ejemplo, las actividades de los usuarios en los procesos empresariales generan tareas. Sin embargo, al trabajar con tareas, el usuario debe ver tareas de todos los dominios en una lista general, por lo que debe existir un servicio de plataforma correspondiente para el registro de tareas, limpio de la lógica empresarial del 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 empresariales desde la perspectiva del desarrollador de aplicaciones

Como se mencionó anteriormente, el desarrollador de aplicaciones debe estar abstraído de las características técnicas y de ingeniería de la implementación de la interacción entre varias aplicaciones, para que se pueda contar con una buena productividad de desarrollo.

Intentemos resolver una tarea de integración bastante complicada, diseñada específicamente para este artículo. Se tratará de una tarea 'juguete' con la participación de tres aplicaciones, cada una de las cuales define un nombre de dominio: 'app1', 'app2', 'app3'.

Dentro de cada aplicación se inician procesos empresariales, que comienzan a "jugar a la pelota" a través de un bus de integración. En rol de la pelota serán los mensajes con el nombre "Ball".

Reglas del juego:

  • el primer jugador – el iniciador. Invita a otros jugadores a jugar, comienza el juego y puede finalizarlo en cualquier momento;
  • los demás jugadores declaran su participación en el juego, se "conocen" entre sí y con el primer jugador;
  • al recibir la pelota, un jugador elige a otro jugador participante y le pasa la pelota. Se lleva un registro del número total de pases;
  • cada jugador tiene "energía", que disminuye con cada pase del balón que realiza. Una vez que se acaba la energía, el jugador abandona el juego, anunciando su salida;
  • si el jugador queda solo, inmediatamente anuncia su salida;
  • cuando todos los jugadores han salido, el primer jugador declara el final del juego. Si salió del juego antes, debe quedarse para supervisar y concluirlo.

Para abordar esta tarea, utilizaré nuestro DSL para procesos de negocio, que permite describir la lógica en Kotlin de manera compacta, con el mínimo de boilerplate.

En la aplicación app1 se ejecutará el proceso de negocio del primer jugador (el que inicia el juego):

class InitialPlayer

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

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

class PlayersList : ArrayList()

// Esta es la clase de instancia del proceso: encapsula su estado interno
class InitialPlayer : ProcessImpl(initialPlayerModel) {
    var playerName: String by persistent("Player1")
    var energy: Int by persistent(30)
    var players: PlayersList by persistent(PlayersList())
    var shotCounter: Int = 0
}

// Esta es la declaración del modelo del proceso: se crea una vez, utilizada por todas
// las instancias del proceso de la clase correspondiente
val initialPlayerModel = processModel(name = "InitialPlayer",
                                                     version = 1) {

    // Según las reglas, el primer jugador es el iniciador del juego y debe ser el único
    uniqueConstraint = UniqueConstraints.singleton

    // Declaramos las actividades que componen el proceso de negocio
    val sendNewGameSignal = signal("NewGame")
    val sendStopGameSignal = signal("StopGame")
    val startTask = humanTask("Start") {
        taskOperation {
            processCondition { players.size > 0 }
            confirmation { "Se han unido ${players.size} jugadores. ¿Comenzamos?" }
        }
    }
    val stopTask = humanTask("Stop") {
        taskOperation {}
    }
    val waitPlayerJoin = signalWait("PlayerJoin") { signal ->
        players.add(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... se unió el jugador ${signal.data} ...")
    }
    val waitPlayerOut = signalWait("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... el jugador ${signal.data} está fuera ...")
    }
    val sendPlayerOut = signal("PlayerOut") {
        signalData = { playerName }
    }
    val sendHandshake = messageSend("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val throwStartBall = messageSend("Ball") {
        messageData = { 1 }
        activation = { selectNextPlayer() }
    }
    val throwBall = messageSend("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData("Ball") {
        shotCounter = it
    }

    // Ahora construimos el gráfico del proceso a partir de las actividades declaradas
    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)
    }

    // Adjuntamos manejadores adicionales a las actividades para registro
    sendNewGameSignal.onExit { println("¡Vamos a jugar!") }
    sendStopGameSignal.onExit { println("¡Detener!") }
    sendPlayerOut.onExit { println("$playerName: ¡Estoy fuera!") }
}

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

Además de ejecutar la lógica empresarial, el código proporcionado puede emitir un modelo de objeto del proceso de negocio que puede ser visualizado como un diagrama. Aún no hemos implementado el visualizador, por lo que tuvimos que dedicar algo de tiempo a dibujar (aquí he simplificado un poco la notación BPMN en lo que respecta al uso de compuertas, para mejorar la coherencia del diagrama con el código proporcionado):

Integración al estilo BPM

La aplicación app2 incluirá el proceso de negocio de otro jugador:

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

class RandomPlayer : ProcessImpl(randomPlayerModel) {

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

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

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

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

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

    sendPlayerJoin.onExit { println("$playerName: ¡Estoy aquí!") }
    sendPlayerOut.onExit { println("$playerName: ¡Estoy fuera!") }
}

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

Diagrama:

Integración al estilo BPM

En la aplicación app3, haremos que el jugador tenga un comportamiento algo diferente: en lugar de elegir aleatoriamente al siguiente jugador, actuará según el algoritmo round-robin:

class RoundRobinPlayer

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

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

class PlayersList: ArrayList()

class RoundRobinPlayer : ProcessImpl(roundRobinPlayerModel) {

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

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

val roundRobinPlayerModel = processModel(
        name = "RoundRobinPlayer", 
        version = 1) {

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

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

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

private fun MessageSendInstance.selectNextPlayer() {
    var idx = process.nextPlayerIndex + 1
    if (idx >= process.players.size) {
        idx = 0
    }
    process.nextPlayerIndex = idx
    val player = if (process.players.isNotEmpty()) 
        process.players[idx] 
    else 
        process.selfPlayer
    receiverDomain = player.domain
    receiverProcessInstanceId = player.id
    println("Step ${process.shotCounter + 1}: " +
            "${process.playerName} >>> ${player.name}")
}

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

Ahora se necesita una prueba para ejecutar todo esto. Solo proporcionaré el código de la prueba para no sobrecargar el artículo con boilerplate (en realidad, utilicé el entorno de prueba creado anteriormente para probar la integración de otros procesos de negocio):

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");
    // Ahora hay que esperar un poco para que los jugadores se "conozcan" entre sí.
    // Esperar a través de sleep es una mala solución, pero es la más simple. 
    // ¡No lo hagan en pruebas serias!
    Thread.sleep(1000);
    // Iniciamos el juego, cerrando la actividad del usuario
    assertTrue(closeTask(app1, pl1, "Start"));
    app1.getWaiting().waitProcessFinished(pl1);
    app2.getWaiting().waitProcessFinished(pl2);
    app2.getWaiting().waitProcessFinished(pl3);
    app3.getWaiting().waitProcessFinished(pl4);
    app3.getWaiting().waitProcessFinished(pl5);
}

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

Ejecutamos la prueba, vemos el registro:

console output

Se ha tomado la clave de bloqueo lock://app1/process/InitialPlayer
¡Vamos a jugar!
Se ha liberado la clave de bloqueo lock://app1/process/InitialPlayer
Player2: ¡Estoy aquí!
Player3: ¡Estoy aquí!
Player4: ¡Estoy aquí!
Player5: ¡Estoy aquí!
... unirse al jugador Player2 ...
... unirse al jugador Player4 ...
... unirse al jugador Player3 ...
... unirse al jugador Player5 ...
Paso 1: Player1 >>> Player3
Paso 2: Player3 >>> Player5
Paso 3: Player5 >>> Player3
Paso 4: Player3 >>> Player4
Paso 5: Player4 >>> Player3
Paso 6: Player3 >>> Player4
Paso 7: Player4 >>> Player5
Paso 8: Player5 >>> Player2
Paso 9: Player2 >>> Player5
Paso 10: Player5 >>> Player4
Paso 11: Player4 >>> Player2
Paso 12: Player2 >>> Player4
Paso 13: Player4 >>> Player1
Paso 14: Player1 >>> Player4
Paso 15: Player4 >>> Player3
Paso 16: Player3 >>> Player1
Paso 17: Player1 >>> Player2
Paso 18: Player2 >>> Player3
Paso 19: Player3 >>> Player1
Paso 20: Player1 >>> Player5
Paso 21: Player5 >>> Player1
Paso 22: Player1 >>> Player2
Paso 23: Player2 >>> Player4
Paso 24: Player4 >>> Player5
Paso 25: Player5 >>> Player3
Paso 26: Player3 >>> Player4
Paso 27: Player4 >>> Player2
Paso 28: Player2 >>> Player5
Paso 29: Player5 >>> Player2
Paso 30: Player2 >>> Player1
Paso 31: Player1 >>> Player3
Paso 32: Player3 >>> Player4
Paso 33: Player4 >>> Player1
Paso 34: Player1 >>> Player3
Paso 35: Player3 >>> Player4
Paso 36: Player4 >>> Player3
Paso 37: Player3 >>> Player2
Paso 38: Player2 >>> Player5
Paso 39: Player5 >>> Player4
Paso 40: Player4 >>> Player5
Paso 41: Player5 >>> Player1
Paso 42: Player1 >>> Player5
Paso 43: Player5 >>> Player3
Paso 44: Player3 >>> Player5
Paso 45: Player5 >>> Player2
Paso 46: Player2 >>> Player3
Paso 47: Player3 >>> Player2
Paso 48: Player2 >>> Player5
Paso 49: Player5 >>> Player4
Paso 50: Player4 >>> Player2
Paso 51: Player2 >>> Player5
Paso 52: Player5 >>> Player1
Paso 53: Player1 >>> Player5
Paso 54: Player5 >>> Player3
Paso 55: Player3 >>> Player5
Paso 56: Player5 >>> Player2
Paso 57: Player2 >>> Player1
Paso 58: Player1 >>> Player4
Paso 59: Player4 >>> Player1
Paso 60: Player1 >>> Player4
Paso 61: Player4 >>> Player3
Paso 62: Player3 >>> Player2
Paso 63: Player2 >>> Player5
Paso 64: Player5 >>> Player4
Paso 65: Player4 >>> Player5
Paso 66: Player5 >>> Player1
Paso 67: Player1 >>> Player5
Paso 68: Player5 >>> Player3
Paso 69: Player3 >>> Player4
Paso 70: Player4 >>> Player2
Paso 71: Player2 >>> Player5
Paso 72: Player5 >>> Player2
Paso 73: Player2 >>> Player1
Paso 74: Player1 >>> Player4
Paso 75: Player4 >>> Player1
Paso 76: Player1 >>> Player2
Paso 77: Player2 >>> Player5
Paso 78: Player5 >>> Player4
Paso 79: Player4 >>> Player3
Paso 80: Player3 >>> Player1
Paso 81: Player1 >>> Player5
Paso 82: Player5 >>> Player1
Paso 83: Player1 >>> Player4
Paso 84: Player4 >>> Player5
Paso 85: Player5 >>> Player3
Paso 86: Player3 >>> Player5
Paso 87: Player5 >>> Player2
Paso 88: Player2 >>> Player3
Player2: ¡Estoy fuera!
Paso 89: Player3 >>> Player4
... el jugador Player2 está fuera ...
Paso 90: Player4 >>> Player1
Paso 91: Player1 >>> Player3
Paso 92: Player3 >>> Player1
Paso 93: Player1 >>> Player4
Paso 94: Player4 >>> Player3
Paso 95: Player3 >>> Player5
Paso 96: Player5 >>> Player1
Paso 97: Player1 >>> Player5
Paso 98: Player5 >>> Player3
Paso 99: Player3 >>> Player5
Paso 100: Player5 >>> Player4
Paso 101: Player4 >>> Player5
Player4: ¡Estoy fuera!
... el jugador Player4 está fuera ...
Paso 102: Player5 >>> Player1
Paso 103: Player1 >>> Player3
Paso 104: Player3 >>> Player1
Paso 105: Player1 >>> Player3
Paso 106: Player3 >>> Player5
Paso 107: Player5 >>> Player3
Paso 108: Player3 >>> Player1
Paso 109: Player1 >>> Player3
Paso 110: Player3 >>> Player5
Paso 111: Player5 >>> Player1
Paso 112: Player1 >>> Player3
Paso 113: Player3 >>> Player5
Paso 114: Player5 >>> Player3
Paso 115: Player3 >>> Player1
Paso 116: Player1 >>> Player3
Paso 117: Player3 >>> Player5
Paso 118: Player5 >>> Player1
Paso 119: Player1 >>> Player3
Paso 120: Player3 >>> Player5
Paso 121: Player5 >>> Player3
Player5: ¡Estoy fuera!
... el jugador Player5 está fuera ...
Paso 122: Player3 >>> Player5
Paso 123: Player5 >>> Player1
Player5: ¡Estoy fuera!
Paso 124: Player1 >>> Player3
... el jugador Player5 está fuera ...
Paso 125: Player3 >>> Player1
Paso 126: Player1 >>> Player3
Player1: ¡Estoy fuera!
... el jugador Player1 está fuera ...
Paso 127: Player3 >>> Player3
Player3: ¡Estoy fuera!
Paso 128: Player3 >>> Player3
... el jugador Player3 está fuera ...
Player3: ¡Estoy fuera!
¡Deténte!
Paso 129: Player3 >>> Player3
Player3: ¡Estoy fuera!

De todo esto se pueden sacar algunas conclusiones importantes:

  • con las herramientas adecuadas, los desarrolladores pueden crear interacciones de integración entre aplicaciones sin desconectarse de la lógica de negocio;
  • la complejidad de la tarea de integración, que requiere competencias de ingeniería, se puede ocultar dentro del marco si se incorpora desde el principio en la arquitectura del mismo. Sin embargo, la dificultad de la tarea no se puede ocultar, por lo que la solución a una tarea difícil en el código se verá reflejada en consecuencia;
  • al desarrollar la lógica de integración, es imprescindible tener en cuenta la consistencia eventual y la falta de linealidad en el cambio de estado de todos los participantes en la integración. Esto obliga a complicar la lógica para hacerla insensible al orden de los eventos externos. En nuestro ejemplo, el jugador debe participar en el juego solo después de haber declarado su salida: otros jugadores seguirá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 deriva de las reglas del juego y es una solución de compromiso dentro de la arquitectura elegida.

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

Todos los mensajes están en una sola cola

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

Integración al estilo BPM

Sin embargo, hay un detalle: cada aplicación filtra sus propios mensajes de la cola desde la entrada, según el nombre de su dominio. También se puede especificar el dominio en las señales si es necesario restringir el "ámbito" de la señal a una única aplicación. Esto debería aumentar la capacidad de la bus, pero la lógica empresarial ahora debe operar con los nombres de dominio: para la dirección de mensajes es obligatorio, y para señales es deseable.

Garantizar la confiabilidad del bus de integración

La confiabilidad se compone de varios aspectos:

  • el broker de mensajes elegido es un componente crítico de la arquitectura y un único punto de falla: debe ser lo suficientemente tolerante a fallos. Se deben utilizar solo implementaciones probadas en el tiempo, con buen soporte y una gran comunidad;
  • es necesario garantizar alta disponibilidad del broker de mensajes, para lo cual debe estar físicamente separado de las aplicaciones integradas (garantizar alta disponibilidad de las aplicaciones con lógica de negocio es significativamente más complejo y costoso);
  • el broker debe proporcionar garantías de entrega "al menos una vez". Este es un requisito indispensable para el funcionamiento confiable del bus de integración. No es necesario tener garantías de nivel "exactamente una vez": los procesos de negocio generalmente no son sensibles a la recepción repetida de mensajes o eventos, y en casos especiales donde esto importa, es más fácil agregar una verificación adicional en la lógica empresarial que utilizar constantemente garantías suficientemente "costosas";
  • el envío de mensajes y señales debe incluirse en una transacción general con la modificación del estado de los procesos de negocio y los datos del dominio. La opción preferida sería utilizar el patrón Transactional Outbox, pero esto requerirá una tabla adicional en la base de datos y un retransmisor. En aplicaciones JEE, se puede simplificar este aspecto utilizando un administrador JTA local, pero la conexión con el broker elegido debe poder operar en modo XA;
  • los manejadores de mensajes y eventos entrantes también deben trabajar con la transacción de cambio de estado del proceso de negocio: si tal transacción se revierte, la recepción del mensaje también debe ser cancelada;
  • los mensajes que no pudieron ser entregados debido a errores deben almacenarse en un repositorio separado DLQ (Cola de Cartas Muertas). Para ello, hemos creado un microservicio de plataforma separado que guarda estos mensajes en su almacenamiento, los indexa por atributos (para una rápida agrupación y búsqueda), y ofrece una API para visualizar, reenvíar al destino y eliminar mensajes. Los administradores del sistema pueden interactuar con este servicio a través de su interfaz web;
  • en la configuración del broker, es necesario ajustar el número de reintentos de entrega y el tiempo de espera entre entregas para reducir la posibilidad de que los mensajes caigan en la DLQ (calcular los parámetros óptimos es prácticamente imposible, pero se puede actuar empíricamente y ajustarlos durante la explotación);
  • el almacenamiento de la DLQ debe ser monitorizado continuamente, y el sistema de monitoreo debe alertar a los administradores del sistema para que reaccionen lo más rápido posible ante la aparición de mensajes no entregados. Esto ayudará a reducir la "zona de impacto" de cualquier fallo o error en la lógica de negocio;
  • el bus de integración debe ser insensible a la ausencia temporal de aplicaciones: las suscripciones al tema deben ser duraderas y el nombre de dominio de la aplicación debe ser único para que, durante la ausencia de la aplicación, sus mensajes en la cola no sean procesados por otra instancia.

Asegurando la seguridad de los hilos en la lógica de negocio

El mismo ejemplo de proceso de negocio puede recibir varios mensajes y eventos al mismo tiempo, cuyo procesamiento se iniciará en paralelo. Al mismo tiempo, para el desarrollador de aplicaciones, todo debe ser sencillo y seguro para los hilos.

La lógica de negocio del proceso maneja cada evento externo que afecta a este proceso de negocio de manera individual. Estos eventos pueden ser:

  • el inicio de una instancia del proceso de negocio;
  • una acción del usuario relacionada con la actividad dentro del proceso de negocio;
  • la llegada de un mensaje o señal al que está suscrita la instancia del proceso de negocio;
  • la activación de un temporizador establecido por la instancia del proceso de negocio;
  • una intervención controlada a través de API (por ejemplo, la interrupción de emergencia del proceso).

Cada uno de estos eventos puede cambiar el estado de la instancia del proceso de negocio: algunas actividades pueden finalizar y otras comenzar, y pueden cambiar los valores de las propiedades persistentes. El cierre de cualquier actividad puede desencadenar una o varias actividades siguientes. Estas, a su vez, pueden detenerse en espera de otros eventos, o si no necesitan datos adicionales, pueden finalizar en la misma transacción. Antes de cerrar la transacción, el nuevo estado del proceso de negocio se guarda en la base de datos, donde esperará la ocurrencia del siguiente evento externo.

Los datos persistentes del proceso de negocio, guardados en una base de datos relacional, son un punto de sincronización muy conveniente para el procesamiento, si se utiliza SELECT FOR UPDATE. Si una transacción ha podido obtener el estado del proceso de negocio de la base de datos para modificarlo, ninguna otra transacción podrá obtener al mismo tiempo ese mismo estado para otra modificación, y tras la finalización de la primera transacción, la segunda recibirá garantizadamente el estado ya modificado.

Al utilizar bloqueos pesimistas en el lado del SGBD, cumplimos con todos los requisitos necesarios ACID, además de mantener la posibilidad de escalar la aplicación con lógica empresarial aumentando el número de instancias en ejecución.

Sin embargo, los bloqueos pesimistas nos amenazan con deadlocks, por lo que SELECT FOR UPDATE debería limitarse a un tiempo de espera razonable en caso de que ocurran deadlocks en algunos casos extremos de la lógica empresarial.

Otro problema es la sincronización del inicio del proceso de negocio. Mientras no haya una instancia del proceso de negocio, no habrá su estado en la base de datos, por lo que el método descrito no servirá. Si se necesita garantizar la unicidad de la instancia del proceso de negocio dentro de un ámbito determinado, se requerirá algún objeto de sincronización asociado con la clase del proceso y el ámbito correspondiente. Para resolver este problema, utilizamos otro mecanismo de bloqueos que permite tomar un bloqueo de un recurso arbitrario, especificado por una clave en formato URI, a través de un servicio externo.

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

uniqueConstraint = UniqueConstraints.singleton

Por lo tanto, en el registro aparecen mensajes sobre la toma y liberación del bloqueo de la clave correspondiente. No hay tales mensajes en otros procesos de negocio: no se ha definido uniqueConstraint.

Problemas de procesos de negocio con estado persistente

A veces, tener un estado persistente no solo ayuda, sino que también puede ser un gran obstáculo en el desarrollo.
Los problemas comienzan cuando es necesario realizar cambios en la lógica del negocio y/o el modelo de procesos de negocio. No todos esos cambios son compatibles con el antiguo estado de los procesos de negocio. Si hay muchos ejemplares "vivos" en la base de datos, realizar cambios incompatibles puede causar muchos problemas, algo con lo que a menudo nos hemos encontrado al utilizar jBPM.

Dependiendo de la profundidad de los cambios, se pueden seguir dos enfoques:

  1. crear un nuevo tipo de proceso de negocio para no hacer cambios incompatibles en el antiguo, y usarlo en lugar del antiguo al iniciar nuevos ejemplares. Los antiguos continuarán funcionando "como antes";
  2. migrar el estado persistente de los procesos de negocio al actualizar la lógica del negocio.

El primer enfoque es más simple, pero tiene sus limitaciones y desventajas, como:

  • la duplicación de la lógica de negocio en muchos modelos de procesos de negocio, incrementando la complejidad de la lógica;
  • a menudo se requiere una transición instantánea a la nueva lógica de negocio (en tareas de integración, casi siempre);
  • el desarrollador no sabe en qué momento se pueden eliminar los modelos obsoletos.

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

  • en la base de datos, el estado persistente del proceso de negocio se guarda en un formato fácil de leer y procesar: en una línea de formato JSON. Esto permite realizar migraciones tanto dentro de la aplicación como fuera de ella. En última instancia, se puede ajustar manualmente (especialmente útil en desarrollo durante la depuración);
  • la lógica de negocio de integración no utiliza nombres de procesos de negocio, para que en cualquier momento se pueda reemplazar la implementación de uno de los procesos participantes por uno nuevo, con un nuevo nombre (por ejemplo, "InitialPlayerV2"). La vinculación se realiza a través de nombres de mensajes y señales;
  • El modelo de proceso tiene un número de versión que incrementamos si realizamos cambios incompatibles en este modelo, y este número se guarda junto con el estado de la instancia del proceso;
  • El estado persistente del proceso se lee de la base de datos inicialmente en un modelo de objeto conveniente con el que puede trabajar el procedimiento de migración, si ha cambiado el número de versión del modelo;
  • El procedimiento de migración se coloca junto a la lógica de negocio y se invoca 'perezosamente' para cada instancia del proceso de negocio en el momento de su recuperación de la base de datos;
  • Si es necesario migrar el estado de todas las instancias del proceso de manera rápida y sincrónica, se aplican soluciones más clásicas para la migración de bases de datos, pero ahí se debe trabajar con JSON.

¿Se necesita otro marco para los procesos de negocio?

Las soluciones descritas en el artículo nos han permitido simplificar notablemente nuestra vida, ampliar el alcance de los problemas abordados a nivel de desarrollo aplicado y hacer más atractivas las ideas de destacar la lógica de negocio en microservicios. Para ello, se ha realizado un gran trabajo, se ha creado un marco 'ligero' para procesos de negocio, así como componentes auxiliares para resolver los problemas mencionados en el contexto de una amplia gama de tareas aplicadas. Tenemos el deseo de compartir estos resultados, llevar el desarrollo de componentes comunes a un acceso abierto bajo una licencia libre. Esto requerirá ciertos esfuerzos y tiempo. Comprender la demanda de tales soluciones podría convertirse en un estímulo adicional para nosotros. En el artículo propuesto, se dedica muy poca atención a las capacidades del marco en sí, pero algunas de ellas son visibles en los ejemplos presentados. Si finalmente publicamos nuestro marco, se le dedicará un artículo separado. Mientras tanto, estaremos agradecidos si nos dejan un pequeño feedback respondiendo a la pregunta:

Solo los usuarios registrados pueden participar en la encuesta. Inicie sesión, por favor.

¿Se necesita otro marco para los procesos de negocio?

  • 18,8%Sí, hemos estado buscando algo así desde hace tiempo3

  • 12,5%Es interesante saber más sobre su implementación, podría ser útil2

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

  • 18,8%Usamos uno de los marcos existentes, estamos satisfechos3

  • 18,8%Nos las arreglamos sin un marco3

  • 25,0%Estamos escribiendo el nuestro4

Votaron 16 usuarios. Se abstuvieron 7 usuarios.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster