Intégration du style BPM

Intégration du style BPM

Salut, Habr!

Notre société est spécialisée dans le développement de solutions logicielles de classe ERP, dans lesquelles la part du lion est occupée par des systèmes transactionnels avec une énorme quantité de logique métier et de flux de travail à la EDMS. Les versions modernes de nos produits sont basées sur les technologies JavaEE, mais nous expérimentons également activement les microservices. L'un des domaines les plus problématiques de ces solutions est l'intégration de divers sous-systèmes liés à des domaines adjacents. Les tâches d'intégration nous ont toujours donné un énorme casse-tête, quels que soient les styles architecturaux, les piles technologiques et les frameworks que nous utilisons, mais récemment, des progrès ont été accomplis dans la résolution de ces problèmes.

Dans l'article porté à votre attention, je parlerai de l'expérience et de la recherche architecturale de NPO Krista dans la zone désignée. Nous allons également considérer un exemple de solution simple à un problème d'intégration du point de vue d'un développeur d'application et découvrir ce qui se cache derrière cette simplicité.

Avertissement

Les solutions architecturales et techniques décrites dans l'article sont proposées par moi sur la base de mon expérience personnelle dans le cadre de tâches spécifiques. Ces solutions ne prétendent pas à l'universalité et peuvent ne pas être optimales dans d'autres conditions d'utilisation.

Qu'est-ce que le BPM a à voir avec cela ?

Pour répondre à cette question, nous devons nous plonger un peu dans les spécificités des problèmes appliqués de nos solutions. La partie principale de la logique métier de notre système transactionnel typique consiste à saisir des données dans la base de données via des interfaces utilisateur, à vérifier manuellement et automatiquement ces données, à les transmettre à un flux de travail, à les publier dans un autre système/base de données analytique/archive, à générer des rapports. Ainsi, la fonction clé du système pour les clients est l'automatisation de leurs processus commerciaux internes.

Par commodité, nous utilisons le terme « document » dans la communication comme une abstraction d'un ensemble de données, unies par une clé commune, à laquelle un flux de travail spécifique peut être « rattaché ».
Mais qu'en est-il de la logique d'intégration ? Après tout, la tâche d'intégration est générée par l'architecture du système, qui est "sciée" en parties PAS à la demande du client, mais sous l'influence de facteurs complètement différents :

  • sous l'influence de la loi de Conway ;
  • du fait de la réutilisation de sous-systèmes précédemment développés pour d'autres produits ;
  • tel que décidé par l'architecte, sur la base d'exigences non fonctionnelles.

La tentation est grande de séparer la logique d'intégration de la logique métier du workflow principal pour ne pas polluer la logique métier avec des artefacts d'intégration et éviter au développeur d'application d'avoir à se plonger dans les particularités du paysage architectural du système. Cette approche présente un certain nombre d'avantages, mais la pratique montre son inefficacité :

  • la résolution des problèmes d'intégration se résume généralement aux options les plus simples sous la forme d'appels synchrones en raison des points d'extension limités dans la mise en œuvre du flux de travail principal (plus d'informations sur les lacunes de l'intégration synchrone ci-dessous) ;
  • les artefacts d'intégration pénètrent toujours dans la logique métier principale lorsqu'un retour d'information d'un autre sous-système est requis ;
  • le développeur de l'application ignore l'intégration et peut facilement la casser en modifiant le flux de travail ;
  • le système cesse d'être un tout unique du point de vue de l'utilisateur, les "coutures" entre les sous-systèmes deviennent perceptibles, des opérations utilisateur redondantes apparaissent qui initient le transfert de données d'un sous-système à un autre.

Une autre approche consiste à considérer les interactions d'intégration comme faisant partie intégrante de la logique métier et du workflow de base. Pour éviter que les compétences requises des développeurs d'applications ne montent en flèche, la création de nouvelles interactions d'intégration doit se faire facilement et naturellement, avec un minimum d'options pour choisir une solution. C'est plus difficile qu'il n'y paraît: l'outil doit être suffisamment puissant pour offrir à l'utilisateur la variété d'options nécessaires à son utilisation et en même temps ne pas se laisser tirer dans le pied. Il existe de nombreuses questions auxquelles un ingénieur doit répondre dans le cadre de tâches d'intégration, mais auxquelles un développeur d'application ne doit pas penser dans son travail quotidien : limites des transactions, cohérence, atomicité, sécurité, mise à l'échelle, répartition de la charge et des ressources, routage, marshaling, contextes de propagation et de commutation, etc. Il est nécessaire de proposer aux développeurs d'applications des modèles de décision assez simples, dans lesquels les réponses à toutes ces questions sont déjà cachées. Ces patrons doivent être suffisamment sécurisés : la logique métier change très souvent, ce qui augmente le risque d'introduire des erreurs, le coût des erreurs doit rester à un niveau assez bas.

Mais encore, qu'est-ce que le BPM a à voir avec cela ? Il existe de nombreuses options pour mettre en œuvre le flux de travail ...
En effet, une autre implémentation des processus métier est très populaire dans nos solutions - à travers le paramétrage déclaratif du diagramme de transition d'état et la connexion des gestionnaires avec la logique métier aux transitions. En même temps, l'état qui détermine la position actuelle du "document" dans le processus métier est un attribut du "document" lui-même.

Intégration du style BPM
Voici à quoi ressemble le processus au début du projet

La popularité d'une telle implémentation est due à la simplicité et à la rapidité relatives de la création de processus métier linéaires. Cependant, à mesure que les systèmes logiciels deviennent plus complexes, la partie automatisée du processus métier se développe et devient plus complexe. Il y a un besoin de décomposition, de réutilisation de parties de processus, ainsi que de processus de bifurcation afin que chaque branche soit exécutée en parallèle. Dans de telles conditions, l'outil devient gênant et le diagramme de transition d'état perd son contenu informatif (les interactions d'intégration ne sont pas du tout reflétées dans le diagramme).

Intégration du style BPM
Voici à quoi ressemble le processus après plusieurs itérations de clarification des exigences

Le moyen de sortir de cette situation était l'intégration du moteur jBPM dans certains produits avec les processus commerciaux les plus complexes. A court terme, cette solution a connu un certain succès : il est devenu possible de mettre en place des processus métiers complexes tout en conservant un schéma assez informatif et à jour dans la notation BPMN2.

Intégration du style BPM
Une petite partie d'un processus métier complexe

À long terme, la solution n'a pas été à la hauteur des attentes : la forte intensité de main-d'œuvre liée à la création de processus métier via des outils visuels n'a pas permis d'atteindre des indicateurs de productivité acceptables, et l'outil lui-même est devenu l'un des moins appréciés des développeurs. Il y a également eu des plaintes concernant la structure interne du moteur, ce qui a entraîné l'apparition de nombreux «patchs» et «béquilles».

Le principal aspect positif de l'utilisation de jBPM était la réalisation des avantages et des inconvénients d'avoir son propre état persistant pour une instance de processus métier. Nous avons également vu la possibilité d'utiliser une approche processus pour implémenter des protocoles d'intégration complexes entre différentes applications en utilisant des interactions asynchrones via des signaux et des messages. La présence d'un état persistant joue un rôle crucial à cet égard.

Sur la base de ce qui précède, nous pouvons conclure : L'approche processus dans le style BPM nous permet de résoudre un large éventail de tâches pour automatiser des processus métier toujours plus complexes, d'intégrer harmonieusement les activités d'intégration dans ces processus et de conserver la possibilité d'afficher visuellement le processus mis en œuvre dans une notation appropriée.

Inconvénients des appels synchrones en tant que modèle d'intégration

L'intégration synchrone fait référence à l'appel de blocage le plus simple. Un sous-système agit comme côté serveur et expose l'API avec la méthode souhaitée. Un autre sous-système agit en tant que côté client et, au bon moment, passe un appel dans l'attente d'un résultat. Selon l'architecture du système, les côtés client et serveur peuvent être hébergés soit dans la même application et le même processus, soit dans des processus différents. Dans le second cas, vous devez appliquer une implémentation de RPC et fournir un marshaling des paramètres et du résultat de l'appel.

Intégration du style BPM

Un tel modèle d'intégration présente un ensemble assez important d'inconvénients, mais il est très largement utilisé en pratique en raison de sa simplicité. La rapidité de mise en œuvre captive et vous fait l'appliquer encore et encore dans des conditions de délais "brûlants", écrivant la solution en dette technique. Mais il arrive aussi que des développeurs inexpérimentés l'utilisent inconsciemment, ne réalisant tout simplement pas les conséquences négatives.

En plus de l'augmentation la plus évidente de la connectivité des sous-systèmes, il existe des problèmes moins évidents avec les transactions de « propagation » et « d'étirement ». En effet, si la logique métier fait des changements, alors les transactions sont indispensables, et les transactions, à leur tour, verrouillent certaines ressources applicatives affectées par ces changements. Autrement dit, jusqu'à ce qu'un sous-système attende une réponse d'un autre, il ne pourra pas terminer la transaction et libérer les verrous. Cela augmente considérablement le risque d'une variété d'effets:

  • la réactivité du système est perdue, les utilisateurs attendent longtemps les réponses aux requêtes ;
  • le serveur cesse généralement de répondre aux requêtes des utilisateurs en raison d'un pool de threads débordant : la plupart des threads « se tiennent » sur le verrou de la ressource occupée par la transaction ;
  • les interblocages commencent à apparaître : la probabilité de leur occurrence dépend fortement de la durée des transactions, de la quantité de logique métier et de verrous impliqués dans la transaction ;
  • des erreurs d'expiration de délai de transaction apparaissent ;
  • le serveur « tombe » sur OutOfMemory si la tâche nécessite de traiter et de modifier de grandes quantités de données, et la présence d'intégrations synchrones rend très difficile le fractionnement du traitement en transactions « plus légères ».

D'un point de vue architectural, l'utilisation du blocage des appels lors de l'intégration entraîne une perte de contrôle de la qualité des sous-systèmes individuels : il est impossible d'assurer les objectifs de qualité d'un sous-système indépendamment des objectifs de qualité d'un autre sous-système. Si les sous-systèmes sont développés par différentes équipes, c'est un gros problème.

Les choses deviennent encore plus intéressantes si les sous-systèmes intégrés se trouvent dans des applications différentes et que des modifications synchrones doivent être apportées des deux côtés. Comment rendre ces changements transactionnels ?

Si des modifications sont apportées dans des transactions distinctes, une gestion et une compensation robustes des exceptions devront être fournies, ce qui élimine complètement le principal avantage des intégrations synchrones - la simplicité.

On pense aussi aux transactions distribuées, mais nous ne les utilisons pas dans nos solutions : il est difficile d'en assurer la fiabilité.

"Saga" comme solution au problème des transactions

Avec la popularité croissante des microservices, il y a une demande croissante de Modèle de saga.

Ce modèle résout parfaitement les problèmes ci-dessus de longues transactions, et élargit également les possibilités de gestion de l'état du système du côté de la logique métier : la compensation après une transaction infructueuse peut ne pas ramener le système à son état d'origine, mais fournir une alternative parcours de traitement des données. Cela vous permet également de ne pas répéter les étapes de traitement des données terminées avec succès lorsque vous essayez de mener le processus à une "bonne" fin.

Fait intéressant, dans les systèmes monolithiques, ce modèle est également pertinent lorsqu'il s'agit de l'intégration de sous-systèmes faiblement couplés et il y a des effets négatifs causés par de longues transactions et les verrous de ressources correspondants.

En ce qui concerne nos processus métier dans le style BPM, il s'avère très facile de mettre en œuvre les Sagas : les étapes individuelles des Sagas peuvent être définies comme des activités au sein du processus métier, et l'état persistant du processus métier détermine, entre autres autres choses, l'état interne des Sagas. Autrement dit, nous n'avons besoin d'aucun mécanisme de coordination supplémentaire. Tout ce dont vous avez besoin est un courtier de messages prenant en charge les garanties "au moins une fois" en tant que transport.

Mais une telle solution a aussi son propre "prix":

  • la logique métier se complexifie : il faut calculer la rémunération ;
  • il faudra abandonner la cohérence totale, qui peut être particulièrement sensible pour les systèmes monolithiques ;
  • l'architecture devient un peu plus compliquée, il y a un besoin supplémentaire d'un courtier de messages ;
  • des outils de surveillance et d'administration supplémentaires seront nécessaires (bien qu'en général, cela soit même bon : la qualité de service du système augmentera).

Pour les systèmes monolithiques, la justification de l'utilisation de "Sags" n'est pas si évidente. Pour les microservices et autres SOA, où, très probablement, il existe déjà un courtier, et où la cohérence totale a été sacrifiée au début du projet, les avantages de l'utilisation de ce modèle peuvent largement compenser les inconvénients, surtout s'il existe une API pratique au niveau niveau logique métier.

Encapsulation de la logique métier dans les microservices

Lorsque nous avons commencé à expérimenter les microservices, une question raisonnable s'est posée : où placer la logique métier du domaine par rapport au service qui fournit la persistance des données du domaine ?

Lorsque l'on examine l'architecture de divers BPMS, il peut sembler raisonnable de séparer la logique métier de la persistance : créez une couche de microservices indépendants de la plate-forme et du domaine qui forment l'environnement et le conteneur pour l'exécution de la logique métier du domaine, et organisez la persistance des données du domaine comme un élément distinct. couche de microservices très simples et légers. Dans ce cas, les processus métier orchestrent les services de la couche de persistance.

Intégration du style BPM

Cette approche a un très gros plus : vous pouvez augmenter les fonctionnalités de la plate-forme autant que vous le souhaitez, et seule la couche correspondante de microservices de la plate-forme en « grossira ». Les processus métier de n'importe quel domaine ont immédiatement la possibilité d'utiliser les nouvelles fonctionnalités de la plate-forme dès sa mise à jour.

Une étude plus détaillée a révélé des lacunes importantes de cette approche :

  • un service de plate-forme qui exécute la logique métier de nombreux domaines à la fois comporte de grands risques en tant que point de défaillance unique. Les modifications fréquentes de la logique métier augmentent le risque de bogues entraînant des défaillances à l'échelle du système ;
  • problèmes de performances : la logique métier fonctionne avec ses données via une interface étroite et lente :
    • les données seront à nouveau rassemblées et pompées à travers la pile réseau ;
    • le service de domaine renverra souvent plus de données que la logique métier n'en nécessite pour le traitement, en raison de capacités de paramétrage de requête insuffisantes au niveau de l'API externe du service ;
    • plusieurs éléments de logique métier indépendants peuvent redemander à plusieurs reprises les mêmes données pour le traitement (vous pouvez atténuer ce problème en ajoutant des beans session qui mettent en cache les données, mais cela complique davantage l'architecture et crée des problèmes de fraîcheur des données et d'invalidation du cache) ;
  • problèmes transactionnels :
    • les processus métier avec un état persistant stocké par le service de plate-forme sont incohérents avec les données du domaine, et il n'existe aucun moyen simple de résoudre ce problème ;
    • déplacer le verrou des données du domaine hors de la transaction : si la logique métier du domaine doit apporter des modifications, après avoir d'abord vérifié l'exactitude des données réelles, il est nécessaire d'exclure la possibilité d'un changement concurrentiel dans les données traitées. Le blocage externe des données peut aider à résoudre le problème, mais une telle solution comporte des risques supplémentaires et réduit la fiabilité globale du système ;
  • complications supplémentaires lors de la mise à jour : dans certains cas, vous devez mettre à jour le service de persistance et la logique métier de manière synchrone ou dans un ordre strict.

En fin de compte, j'ai dû revenir à l'essentiel : encapsuler les données du domaine et la logique métier du domaine dans un seul microservice. Cette approche simplifie la perception du microservice en tant que composant intégral du système et ne donne pas lieu aux problèmes ci-dessus. Ce n'est pas non plus gratuit :

  • La normalisation des API est requise pour l'interaction avec la logique métier (en particulier, pour fournir des activités utilisateur dans le cadre des processus métier) et les services de la plateforme API ; une attention plus particulière aux modifications de l'API, la compatibilité ascendante et descendante est requise ;
  • il est nécessaire d'ajouter des bibliothèques d'exécution supplémentaires pour assurer le fonctionnement de la logique métier dans le cadre de chacun de ces microservices, ce qui entraîne de nouvelles exigences pour ces bibliothèques : légèreté et un minimum de dépendances transitives ;
  • Les développeurs de logique métier doivent suivre les versions des bibliothèques : si un microservice n'a pas été finalisé depuis longtemps, il contiendra très probablement une version obsolète des bibliothèques. Cela peut être un obstacle inattendu à l'ajout d'une nouvelle fonctionnalité et peut nécessiter la migration de l'ancienne logique métier d'un tel service vers de nouvelles versions des bibliothèques en cas de modifications incompatibles entre les versions.

Intégration du style BPM

Une couche de services de plate-forme est également présente dans une telle architecture, mais cette couche ne forme plus un conteneur pour l'exécution de la logique métier du domaine, mais uniquement son environnement, fournissant des fonctions auxiliaires de « plate-forme ». Une telle couche est nécessaire non seulement pour maintenir la légèreté des microservices de domaine, mais également pour centraliser la gestion.

Par exemple, les activités des utilisateurs dans les processus métier génèrent des tâches. Cependant, lorsqu'il travaille avec des tâches, l'utilisateur doit voir les tâches de tous les domaines dans la liste générale, ce qui signifie qu'il doit y avoir un service de plate-forme d'enregistrement de tâches approprié, effacé de la logique métier du domaine. Garder l'encapsulation de la logique métier dans ce contexte est assez problématique, et c'est un autre compromis de cette architecture.

Intégration des processus métier à travers les yeux d'un développeur d'applications

Comme déjà mentionné ci-dessus, le développeur d'applications doit s'abstraire des caractéristiques techniques et d'ingénierie de la mise en œuvre de l'interaction de plusieurs applications afin de pouvoir compter sur une bonne productivité de développement.

Essayons de résoudre un problème d'intégration assez difficile, spécialement inventé pour l'article. Ce sera une tâche de "jeu" impliquant trois applications, où chacune d'entre elles définit un nom de domaine : "app1", "app2", "app3".

À l'intérieur de chaque application, des processus métier sont lancés qui commencent à "jouer au ballon" via le bus d'intégration. Les messages nommés "Ball" agiront comme le ballon.

Règles du jeu:

  • le premier joueur est l'initiateur. Il invite d'autres joueurs à la partie, démarre la partie et peut la terminer à tout moment ;
  • les autres joueurs déclarent leur participation au jeu, « font connaissance » entre eux et avec le premier joueur ;
  • après avoir reçu le ballon, le joueur choisit un autre joueur participant et lui passe le ballon. Le nombre total de passages est compté ;
  • chaque joueur a une "énergie", qui diminue à chaque passe de balle de ce joueur. Lorsque l'énergie s'épuise, le joueur est éliminé du jeu, annonçant sa retraite ;
  • si le joueur est laissé seul, il déclare immédiatement son départ ;
  • lorsque tous les joueurs sont éliminés, le premier joueur déclare la fin de la partie. S'il a quitté le jeu plus tôt, il reste alors à suivre le jeu afin de le terminer.

Pour résoudre ce problème, j'utiliserai notre DSL pour les processus métier, qui vous permet de décrire la logique dans Kotlin de manière compacte, avec un minimum de passe-partout.

Dans l'application app1, le processus métier du premier joueur (il est aussi l'initiateur du jeu) fonctionnera :

classe InitialPlayer

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

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

class PlayersList : ArrayList<PlayerInfo>()

// Это класс экземпляра процесса: инкапсулирует его внутреннее состояние
class InitialPlayer : ProcessImpl<InitialPlayer>(initialPlayerModel) {
    var playerName: String by persistent("Player1")
    var energy: Int by persistent(30)
    var players: PlayersList by persistent(PlayersList())
    var shotCounter: Int = 0
}

// Это декларация модели процесса: создается один раз, используется всеми
// экземплярами процесса соответствующего класса
val initialPlayerModel = processModel<InitialPlayer>(name = "InitialPlayer",
                                                     version = 1) {

    // По правилам, первый игрок является инициатором игры и должен быть единственным
    uniqueConstraint = UniqueConstraints.singleton

    // Объявляем активности, из которых состоит бизнес-процесс
    val sendNewGameSignal = signal<String>("NewGame")
    val sendStopGameSignal = signal<String>("StopGame")
    val startTask = humanTask("Start") {
        taskOperation {
            processCondition { players.size > 0 }
            confirmation { "Подключилось ${players.size} игроков. Начинаем?" }
        }
    }
    val stopTask = humanTask("Stop") {
        taskOperation {}
    }
    val waitPlayerJoin = signalWait<String>("PlayerJoin") { signal ->
        players.add(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... join player ${signal.data} ...")
    }
    val waitPlayerOut = signalWait<String>("PlayerOut") { signal ->
        players.remove(PlayerInfo(
                signal.data!!,
                signal.sender.domain,
                signal.sender.processInstanceId))
        println("... player ${signal.data} is out ...")
    }
    val sendPlayerOut = signal<String>("PlayerOut") {
        signalData = { playerName }
    }
    val sendHandshake = messageSend<String>("Handshake") {
        messageData = { playerName }
        activation = {
            receiverDomain = process.players.last().domain
            receiverProcessInstanceId = process.players.last().id
        }
    }
    val throwStartBall = messageSend<Int>("Ball") {
        messageData = { 1 }
        activation = { selectNextPlayer() }
    }
    val throwBall = messageSend<Int>("Ball") {
        messageData = { shotCounter + 1 }
        activation = { selectNextPlayer() }
        onEntry { energy -= 1 }
    }
    val waitBall = messageWaitData<Int>("Ball") {
        shotCounter = it
    }

    // Теперь конструируем граф процесса из объявленных активностей
    startFrom(sendNewGameSignal)
            .fork("mainFork") {
                next(startTask)
                next(waitPlayerJoin).next(sendHandshake).next(waitPlayerJoin)
                next(waitPlayerOut)
                        .branch("checkPlayers") {
                            ifTrue { players.isEmpty() }
                                    .next(sendStopGameSignal)
                                    .terminate()
                            ifElse().next(waitPlayerOut)
                        }
            }
    startTask.fork("afterStart") {
        next(throwStartBall)
                .branch("mainLoop") {
                    ifTrue { energy < 5 }.next(sendPlayerOut).next(waitBall)
                    ifElse().next(waitBall).next(throwBall).loop()
                }
        next(stopTask).next(sendStopGameSignal)
    }

    // Навешаем на активности дополнительные обработчики для логирования
    sendNewGameSignal.onExit { println("Let's play!") }
    sendStopGameSignal.onExit { println("Stop!") }
    sendPlayerOut.onExit { println("$playerName: I'm out!") }
}

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

En plus d'exécuter la logique métier, le code ci-dessus peut produire un modèle objet d'un processus métier qui peut être visualisé sous forme de diagramme. Nous n'avons pas encore implémenté le visualiseur, nous avons donc dû passer un peu de temps à dessiner (ici j'ai légèrement simplifié la notation BPMN concernant l'utilisation des portes pour améliorer la cohérence du diagramme avec le code ci-dessus) :

Intégration du style BPM

app2 inclura le processus métier d'un autre joueur :

classe RandomPlayer

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

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

class PlayersList: ArrayList<PlayerInfo>()

class RandomPlayer : ProcessImpl<RandomPlayer>(randomPlayerModel) {

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

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

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

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

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

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

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

Diagramme:

Intégration du style BPM

Dans l'application app3, nous allons faire en sorte que le joueur ait un comportement légèrement différent : au lieu de choisir au hasard le joueur suivant, il agira selon l'algorithme du round robin :

classe RoundRobinPlayer

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

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

class PlayersList: ArrayList<PlayerInfo>()

class RoundRobinPlayer : ProcessImpl<RoundRobinPlayer>(roundRobinPlayerModel) {

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

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

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

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

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

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

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

Sinon, le comportement du joueur ne diffère pas du précédent, donc le diagramme ne change pas.

Maintenant, nous avons besoin d'un test pour tout exécuter. Je ne donnerai que le code du test lui-même, afin de ne pas encombrer l'article avec un passe-partout (en fait, j'ai utilisé l'environnement de test créé précédemment pour tester l'intégration d'autres processus métiers) :

testJeu()

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

Exécutez le test, regardez le journal :

sortie console

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

Plusieurs conclusions importantes peuvent être tirées de tout cela :

  • si les outils nécessaires sont disponibles, les développeurs d'applications peuvent créer des interactions d'intégration entre les applications sans rompre avec la logique métier ;
  • la complexité (complexité) d'une tâche d'intégration qui nécessite des compétences en ingénierie peut être cachée à l'intérieur du framework si elle est initialement définie dans l'architecture du framework. La difficulté de la tâche (difficulté) ne peut pas être masquée, de sorte que la solution à une tâche difficile dans le code apparaîtra en conséquence ;
  • lors du développement de la logique d'intégration, il est nécessaire de prendre en compte éventuellement la cohérence et le manque de linéarisabilité du changement d'état de tous les participants à l'intégration. Cela oblige à compliquer la logique pour la rendre insensible à l'ordre dans lequel se produisent les événements extérieurs. Dans notre exemple, le joueur est obligé de participer au jeu après avoir annoncé sa sortie du jeu : les autres joueurs continueront à lui passer le ballon jusqu'à ce que l'information sur sa sortie parvienne et soit traitée par tous les participants. Cette logique ne découle pas des règles du jeu et est une solution de compromis dans le cadre de l'architecture choisie.

Parlons ensuite des diverses subtilités de notre solution, des compromis et autres points.

Tous les messages dans une file d'attente

Toutes les applications intégrées fonctionnent avec un bus d'intégration, présenté comme un courtier externe, une BPMQueue pour les messages et une rubrique BPMTopic pour les signaux (événements). Faire passer tous les messages dans une seule file d'attente est en soi un compromis. Au niveau de la logique métier, vous pouvez désormais introduire autant de nouveaux types de messages que vous le souhaitez sans apporter de modifications à la structure du système. C'est une simplification importante, mais elle comporte certains risques, qui, dans le cadre de nos tâches typiques, nous semblaient peu importants.

Intégration du style BPM

Cependant, il y a ici une subtilité : chaque application filtre "ses" messages de la file d'attente à l'entrée, par le nom de son domaine. En outre, le domaine peut être spécifié dans les signaux, si vous avez besoin de limiter la "portée" du signal à une seule application. Cela devrait augmenter la bande passante du bus, mais la logique métier doit maintenant fonctionner avec des noms de domaine : obligatoire pour l'adressage des messages, souhaitable pour les signaux.

Assurer la fiabilité du bus d'intégration

La fiabilité est composée de plusieurs choses :

  • Le courtier de messages choisi est un composant critique de l'architecture et un point de défaillance unique : il doit être suffisamment tolérant aux pannes. Vous ne devez utiliser que des implémentations éprouvées avec un bon support et une grande communauté ;
  • il est nécessaire d'assurer une haute disponibilité du courtier de messages, pour lequel il doit être physiquement séparé des applications intégrées (la haute disponibilité des applications avec une logique métier appliquée est beaucoup plus difficile et coûteuse à fournir) ;
  • le courtier est tenu de fournir "au moins une fois" des garanties de livraison. Il s'agit d'une exigence obligatoire pour un fonctionnement fiable du bus d'intégration. Il n'y a pas besoin de garanties de niveau "exactement une fois" : les processus métier ne sont généralement pas sensibles à l'arrivée répétée de messages ou d'événements, et dans les tâches spéciales où cela est important, il est plus facile d'ajouter des contrôles supplémentaires à la logique métier que d'utiliser constamment des garanties plutôt « chères » ;
  • l'envoi de messages et de signaux doit être impliqué dans une transaction commune avec un changement dans l'état des processus métier et des données du domaine. L'option préférée serait d'utiliser le modèle Boîte d'envoi transactionnelle, mais cela nécessitera une table supplémentaire dans la base de données et un relais. Dans les applications JEE, cela peut être simplifié en utilisant un gestionnaire JTA local, mais la connexion au courtier sélectionné doit pouvoir fonctionner en mode XA;
  • les gestionnaires de messages et d'événements entrants doivent également travailler avec la transaction de modification de l'état du processus métier : si une telle transaction est annulée, la réception du message doit également être annulée ;
  • les messages qui n'ont pas pu être livrés en raison d'erreurs doivent être stockés dans un magasin séparé D.L.Q. (file d'attente de lettres mortes). Pour ce faire, nous avons créé un microservice de plate-forme distinct qui stocke ces messages dans son stockage, les indexe par attributs (pour un regroupement et une recherche rapides) et expose l'API pour l'affichage, le renvoi à l'adresse de destination et la suppression des messages. Les administrateurs système peuvent travailler avec ce service via leur interface Web ;
  • dans les paramètres du courtier, vous devez ajuster le nombre de tentatives de livraison et les délais entre les livraisons afin de réduire la probabilité que des messages entrent dans le DLQ (il est presque impossible de calculer les paramètres optimaux, mais vous pouvez agir de manière empirique et les ajuster pendant opération);
  • le magasin DLQ doit être surveillé en permanence et le système de surveillance doit informer les administrateurs système afin qu'ils puissent répondre aussi rapidement que possible en cas de messages non remis. Cela réduira la « zone de dommages » d'une panne ou d'une erreur de logique métier ;
  • le bus d'intégration doit être insensible à l'absence temporaire d'applications : les abonnements aux rubriques doivent être pérennes, et le nom de domaine de l'application doit être unique afin que quelqu'un d'autre ne tente pas de traiter son message de la file d'attente pendant l'absence de l'application.

Assurer la sécurité des threads de la logique métier

Une même instance d'un processus métier peut recevoir plusieurs messages et événements à la fois, dont le traitement démarrera en parallèle. En même temps, pour un développeur d'applications, tout doit être simple et sans fil.

La logique métier de processus traite chaque événement externe qui affecte ce processus métier individuellement. Ces événements peuvent être :

  • lancement d'une instance de processus métier ;
  • une action utilisateur liée à une activité au sein d'un processus métier ;
  • réception d'un message ou d'un signal auquel une instance de processus métier est abonnée ;
  • expiration du temporisateur défini par l'instance de processus métier ;
  • action de contrôle via l'API (par exemple, l'abandon du processus).

Chacun de ces événements peut modifier l'état d'une instance de processus métier : certaines activités peuvent se terminer et d'autres démarrer, les valeurs des propriétés persistantes peuvent changer. La fermeture de toute activité peut entraîner l'activation d'une ou plusieurs des activités suivantes. Ceux-ci, à leur tour, peuvent arrêter d'attendre d'autres événements ou, s'ils n'ont pas besoin de données supplémentaires, ils peuvent se terminer dans la même transaction. Avant de fermer la transaction, le nouvel état du processus métier est stocké dans la base de données, où il attendra le prochain événement externe.

Les données de processus métier persistantes stockées dans une base de données relationnelle constituent un point de synchronisation de traitement très pratique lors de l'utilisation de SELECT FOR UPDATE. Si une transaction a réussi à obtenir l'état du processus métier à partir de la base de données pour le modifier, aucune autre transaction en parallèle ne pourra obtenir le même état pour un autre changement, et après l'achèvement de la première transaction, la seconde est garanti de recevoir l'état déjà modifié.

En utilisant des verrous pessimistes du côté du SGBD, nous remplissons toutes les conditions nécessaires ACID, et conservez également la possibilité de faire évoluer l'application avec la logique métier en augmentant le nombre d'instances en cours d'exécution.

Cependant, les verrous pessimistes nous menacent de blocages, ce qui signifie que SELECT FOR UPDATE doit toujours être limité à un délai d'attente raisonnable en cas de blocages sur certains cas flagrants dans la logique métier.

Un autre problème est la synchronisation du démarrage du processus métier. Bien qu'il n'y ait pas d'instance de processus métier, il n'y a pas non plus d'état dans la base de données, donc la méthode décrite ne fonctionnera pas. Si vous souhaitez garantir l'unicité d'une instance de processus métier dans une étendue particulière, vous avez besoin d'un type d'objet de synchronisation associé à la classe de processus et à l'étendue correspondante. Pour résoudre ce problème, nous utilisons un mécanisme de verrouillage différent qui nous permet de prendre un verrou sur une ressource arbitraire spécifiée par une clé au format URI via un service externe.

Dans nos exemples, le processus métier InitialPlayer contient une déclaration

uniqueConstraint = UniqueConstraints.singleton

Par conséquent, le journal contient des messages sur la prise et la libération du verrou de la clé correspondante. Il n'y a pas de tels messages pour les autres processus métier : uniqueConstraint n'est pas défini.

Problèmes de processus métier avec état persistant

Parfois, avoir un état persistant aide non seulement, mais entrave également le développement.
Les problèmes commencent lorsque vous devez apporter des modifications à la logique métier et/ou au modèle de processus métier. Aucune modification de ce type n'est jugée compatible avec l'ancien état des processus métier. S'il existe de nombreuses instances "actives" dans la base de données, apporter des modifications incompatibles peut entraîner de nombreux problèmes, que nous rencontrons souvent lors de l'utilisation de jBPM.

Selon la profondeur du changement, vous pouvez agir de deux manières :

  1. créez un nouveau type de processus métier afin de ne pas apporter de modifications incompatibles à l'ancien et utilisez-le à la place de l'ancien lors du démarrage de nouvelles instances. Les anciennes instances continueront de fonctionner "à l'ancienne" ;
  2. migrer l'état persistant des processus métier lors de la mise à jour de la logique métier.

La première façon est plus simple, mais a ses limites et ses inconvénients, par exemple :

  • duplication de la logique métier dans de nombreux modèles de processus métier, augmentation du volume de la logique métier ;
  • une transition instantanée vers une nouvelle logique métier est souvent nécessaire (presque toujours en termes de tâches d'intégration) ;
  • le développeur ne sait pas à quel moment il est possible de supprimer des modèles obsolètes.

En pratique, nous utilisons les deux approches, mais avons pris un certain nombre de décisions pour nous simplifier la vie :

  • dans la base de données, l'état persistant du processus métier est stocké sous une forme facilement lisible et facilement exploitable : dans une chaîne au format JSON. Cela vous permet d'effectuer des migrations à la fois à l'intérieur de l'application et à l'extérieur. Dans les cas extrêmes, vous pouvez également le modifier avec des poignées (particulièrement utile en développement lors du débogage) ;
  • la logique métier d'intégration n'utilise pas les noms des processus métier, de sorte qu'il est possible à tout moment de remplacer l'implémentation d'un des processus participants par un nouveau, avec un nouveau nom (par exemple, "InitialPlayerV2"). La liaison s'effectue via les noms des messages et des signaux ;
  • le modèle de processus a un numéro de version, que nous incrémentons si nous apportons des modifications incompatibles à ce modèle, et ce numéro est stocké avec l'état de l'instance de processus ;
  • l'état persistant du processus est d'abord lu à partir de la base dans un modèle d'objet pratique avec lequel la procédure de migration peut fonctionner si le numéro de version du modèle a changé ;
  • la procédure de migration est placée à côté de la logique métier et est dite "lazy" pour chaque instance du processus métier au moment de sa restauration depuis la base de données ;
  • si vous avez besoin de migrer l'état de toutes les instances de processus rapidement et de manière synchrone, des solutions de migration de base de données plus classiques sont utilisées, mais vous devez y travailler avec JSON.

Ai-je besoin d'un autre framework pour les processus métier ?

Les solutions décrites dans l'article nous ont permis de nous simplifier considérablement la vie, d'élargir l'éventail des problèmes résolus au niveau du développement d'applications et de rendre plus attrayante l'idée de séparer la logique métier en microservices. Pour cela, beaucoup de travail a été fait, un cadre très «léger» pour les processus métier a été créé, ainsi que des composants de service pour résoudre les problèmes identifiés dans le cadre d'un large éventail de tâches appliquées. Nous avons une volonté de partager ces résultats, de porter le développement de composants communs en libre accès sous licence libre. Cela nécessitera des efforts et du temps. Comprendre la demande pour de telles solutions pourrait être une incitation supplémentaire pour nous. Dans l'article proposé, très peu d'attention est accordée aux capacités du framework lui-même, mais certaines d'entre elles sont visibles à partir des exemples présentés. Si nous publions néanmoins notre framework, un article séparé lui sera consacré. En attendant, nous vous serions reconnaissants de laisser un petit commentaire en répondant à la question :

Seuls les utilisateurs enregistrés peuvent participer à l'enquête. se connecters'il te plait.

Ai-je besoin d'un autre framework pour les processus métier ?

  • 18,8%Oui, je cherchais quelque chose comme ça depuis longtemps.

  • 12,5%il est intéressant d'en savoir plus sur votre implémentation, cela peut être utile2

  • 6,2%nous utilisons un des frameworks existants, mais nous pensons le remplacer1

  • 18,8%on utilise un des frameworks existants, tout convient3

  • 18,8%faire face sans cadre3

  • 25,0%écrivez vous-même4

16 utilisateurs ont voté. 7 utilisateurs se sont abstenus.

Source: habr.com

Ajouter un commentaire