Cómo hicimos nuestro motor de flujo de trabajo

En DIRECTUM, estamos desarrollando el sistema DirectumRX ECM. El elemento central del módulo Workflow para el sistema ECM es el motor. Es responsable de cambiar el estado de la instancia del proceso (instancia) durante el ciclo de vida. Antes de comenzar a desarrollar el módulo Workflow, debe decidir: tomar un motor listo o escribir el suyo propio. Inicialmente, fuimos por la primera opción. Tomamos el motor de Windows Workflow Foundation (WF) y, en general, nos convenía. Pero con el tiempo, nos dimos cuenta de que necesitábamos nuestro propio motor. Cómo sucedió esto y qué sucedió, lo contaré a continuación.

Motor viejo


¿Por qué wf?


En 2013, cuando llegó el momento de desarrollar un módulo de flujo de trabajo para DirectumRX, decidimos tomar un motor listo para usar. Visto desde Windows Workflow Foundation (WF), ActiveFlow, K2.NET, WorkflowEngine.NET, cDevWorkflow, NetBpm. Algunos no estaban contentos con el valor, algunos estaban en bruto, algunos, para ese momento, no habían sido respaldados por mucho tiempo.
Como resultado, la elección recayó en WF. Luego, utilizamos activamente la pila de Microsoft (WCF, WPF) y decidimos que otra W no nos haría daño. Otra ventaja fue nuestro estatus como Microsoft Gold Application Development Partner, que permitió desarrollar productos utilizando las tecnologías de Microsoft. Bueno, en general, las capacidades del motor nos convenían y cubrían casi todos nuestros casos.

¿Qué le pasa a WF?


Después de 6 años de usar WF, hemos acumulado una serie de problemas, y el costo de resolver estos problemas era demasiado alto. Comenzamos a pensar en desarrollar nuestro propio motor. Hablaré de algunos de ellos.

Diagnóstico costoso y corrección de errores


Pasaron los años, creció la cantidad de instalaciones de productos y la carga. Comenzaron a aparecer errores, cuyo diagnóstico y corrección requirió muchos recursos. Esto se vio facilitado por un complejo de razones: falta de competencias, errores de diseño al integrar el motor anterior y características de WF.
Teníamos suficientes competencias básicas para construir en WF DirectumRX, el mismo nivel era suficiente para lidiar con errores simples. Para casos complejos, las competencias se volvieron insuficientes: el análisis de registros, el análisis del estado de la instancia, etc., eran difíciles.
Era posible enviar a una persona a cursos sobre WF, pero apenas se les enseña cómo analizar el estado de una instancia y asociar su cambio con los registros. Y, francamente, nadie tenía un deseo particular de mejorar sus habilidades con tecnología prácticamente muerta.
Otra forma es contratar a una persona con las competencias adecuadas. Pero encontrar uno en Izhevsk no es una tarea tan trivial, y no el hecho de que su nivel es suficiente para resolver nuestros problemas.
De hecho, nos enfrentamos a un umbral de entrada alto para admitir WF. De una forma u otra, creo que trataríamos este problema, si no por otras razones.
Otro problema fue que al construir diagramas de proceso usamos nuestra propia notación. Es más visual y más fácil de desarrollar. Por ejemplo, WF no permite implementar un gráfico completo, no puede dibujar bloques sin salida, hay características de dibujar ramas paralelas. La recuperación de esto es la conversión de nuestros circuitos a circuitos WF, que no son tan simples e imponen una serie de limitaciones. Al depurar, tuve que analizar el estado del circuito WF, debido a esto, se perdió la visibilidad, tuve que comparar bloques y caras entre sí para comprender en qué paso estaba la instancia.
imagen

Representación del circuito en DirectumRX
imagen

Representación del circuito en WF
Además, nos enfrentamos al hecho de que la documentación de WF describe mal el repositorio de instancias. Como escribí anteriormente, esto es necesario al analizar un error para comprender el estado de la instancia del proceso. Además, parte de los datos está encriptada, lo que también interfiere con el análisis.

Postgres como un DBMS


Durante muchos años, ha habido una tendencia en Rusia para la sustitución de importaciones, y cada vez más uno de los requisitos para la plataforma es el soporte de los sistemas de gestión de bases de datos de código abierto (DBMS) o DBMS de producción nacional. Muy a menudo es Postgres. Fuera de la caja, WF solo es compatible con MS SQL. Para trabajar con otras bases de datos, puede utilizar proveedores externos. Elegimos dotConnect de DevArt.
Si bien la carga fue ligera, todo funcionó bien. Pero tan pronto como manejamos el sistema bajo carga, aparecieron problemas. WF podría detener repentinamente y detener el procesamiento de instancias (transacciones preparadas finalizadas), o todos los mensajes fueron a MSMQ Poisoned Queue, etc. Nos ocupamos de todos estos problemas, pero pasamos mucho tiempo en ello. No había garantía de que no apareciera uno nuevo, cuya solución tendría que gastar la misma cantidad.

Cuidado en .net core


Después de que Microsoft anunció .Net Core, decidimos que iríamos gradualmente para lograr soluciones multiplataforma. Microsoft decidió no tener a WF a bordo, lo que nos impidió transferir el módulo Workflow a .Net Core en la forma en que existía. Somos conscientes de que hay puertos WF no oficiales en .Net Core, y entre ellos incluso hay desarrolladores WF, pero no todos son 100% compatibles. Otro factor fue la negativa de Microsoft a desarrollar .Net. a favor de .Net Core.

Nuevo motor


Tomando todo este montón de problemas, opciones de solución, la complejidad de la refactorización y las correcciones, sopesando todos los pros y los contras, decidimos cambiar a un nuevo motor. Comenzamos analizando los existentes.

La eleccion


Los requisitos principales al elegir un motor fueron:
  • trabajar en .Net Core;
  • escalabilidad
  • conversión de instancias de proceso existentes, con la capacidad de continuar la ejecución después de la conversión
  • costo razonable de analizar problemas existentes
  • trabajar con diferentes DBMS

Además, se requería que la Actividad (Actividad) pudiera ejecutar el código de la aplicación en C #, era posible depurar bloques, etc.
Como parte del análisis de los motores existentes, buscamos:
  1. Core wf
  2. Flowwright
  3. Flujo de trabajo K2
  4. Núcleo de flujo de trabajo
  5. Zeebe
  6. Motor de flujo de trabajo
  7. Marco de tareas duradero
  8. Camunda
  9. Actividades de Orleans

Habiendo impuesto todos los requisitos sobre las soluciones revisadas y agregando el costo de las soluciones pagas, consideramos que nuestro motor no es muy costoso, mientras que será 100% adecuado para nuestras solicitudes y será fácil de refinar.

Implementación / Arquitectura


En la implementación anterior, el módulo WF era un servicio WCF al que estaban conectadas las bibliotecas WF. Pudo crear instancias de proceso, iniciar procesos, ejecutar bloques, incluida la lógica de negocios (código escrito por desarrolladores). Todo esto fue alojado en la aplicación IIS.
En la nueva implementación, siguiendo la tendencia de la arquitectura de microservicios, decidimos dividir inmediatamente el servicio en dos: Servicio de proceso de flujo de trabajo (WPS) y Servicio de bloque de flujo de trabajo (WBS), que podría alojarse por separado. Otro enlace en esta cadena es el Servicio de aplicaciones, que implementa el sistema DirectumRX y la lógica empresarial, y los clientes trabajan con él.
WPS "camina" de acuerdo con el esquema, WBS procesa bloques y ejecuta la lógica empresarial en cada paso. El comando para iniciar el proceso proviene del servidor de aplicaciones. La interacción entre los servicios se lleva a cabo utilizando RabbitMQ. A continuación te contaré más sobre cada uno de ellos.


Wps


Workflow Process Service es un microservicio que se encarga de iniciar procesos y omitir el diagrama de proceso.
El repositorio de servicios contiene diagramas de proceso con soporte para versiones y estado serializado de instancias de proceso. Puede usar MS SQL y Postgres como almacenamiento.
El servicio puede procesar mensajes recibidos de otros servicios a través de RabbitMQ. Básicamente, los mensajes son una API de servicio. Tipos de mensajes que el servicio puede recibir:
  • StartProcess: crea una nueva instancia de proceso y comienza a rastrearla;
  • CompleteBlock: finalización del bloque, después de este mensaje, el servicio mueve la instancia del proceso más allá del esquema;
  • Suspend / ResumeProcess: suspender la ejecución de una instancia de proceso, por ejemplo, debido a un error al procesar un bloque, y reanudar la ejecución después de que se haya solucionado el error;
  • Abort / RestartProcess: detiene la ejecución de la instancia de proceso y comienza de nuevo;
  • DeleteProcess: elimina una instancia de proceso.

El esquema consiste en bloques y conexiones entre ellos (caras). Cada cara tiene un identificador, el llamado "Resultado de ejecución". Hay 5 tipos de bloques:
  • StartBlock
  • Bloque
  • OrBlock;
  • AndBlock;
  • FinBlock.

imagen

Vista de esquema WPS
Cuando llega un mensaje al comienzo del proceso, el servicio crea una instancia y comienza a "caminar" de acuerdo con el esquema. La clase responsable del "caminar" de acuerdo con el esquema, en broma llamamos al "Stepator". Un circuito siempre comienza con un StartBlock. Luego, el strider toma todas las caras salientes y las activa. Cada bloque funciona según el principio del bloque "Y", es decir Todas las caras entrantes deben estar activas para que el bloqueo se pueda activar. El algoritmo decide qué bloques se pueden activar y envía un mensaje WBS para activar estos bloques. WBS procesa el bloque y devuelve el resultado del WPS. Dependiendo del resultado de la ejecución, el strider selecciona las caras apropiadas que salen del bloque para la activación, y el proceso continúa.
Durante el desarrollo, nos encontramos con situaciones interesantes relacionadas con las conexiones cíclicas entre bloques, que agregaron lógica al decidir qué bloque activar / detener.
El servicio es autónomo, es decir. simplemente pásele el esquema en formato Json, escriba su propio controlador de bloque y podrá intercambiar mensajes.

Wbs


Workflow Block Service es un servicio que procesa diagramas de bloques. El servicio conoce la esencia de la lógica empresarial, como tareas, tareas, etc. Estas entidades se pueden agregar al entorno de desarrollo DirectumRX Development Studio (DDS). Por ejemplo, nuestros bloques tienen un evento para comenzar el bloque. El desarrollador escribe el código para este controlador de eventos en DDS, y WBS ejecuta este código. De hecho, esta es nuestra implementación del controlador de bloque; puede reemplazarlo con el suyo.
El servicio almacena el estado de los bloques. Además de las propiedades básicas (Id, Estado), el bloque puede contener otra información necesaria para la ejecución / terminación / suspensión del bloque.
Los bloques pueden estar en un estado:
  • Completado: entra en este estado después de completar con éxito el trabajo en el bloque;
  • Pendiente: está en estado de espera cuando se realiza algún trabajo dentro del bloque, por ejemplo, se requiere algún tipo de respuesta del usuario;
  • Abortado: entra en este estado cuando se detiene el proceso;
  • Suspendido: entra en este estado cuando el proceso se detiene cuando se produce un error.

Cuando llega un mensaje para ejecutar el bloque, el bloque se ejecuta y WBS envía un mensaje con el resultado del bloque.

Escalabilidad


WPS y WBS se pueden implementar en varias instancias. En un momento dado, solo un servicio WPS puede procesar una instancia de proceso. Lo mismo se aplica a los bloques de procesamiento: una instancia de proceso puede procesar solo un bloque a la vez. Esto es ayudado por bloqueos que se ponen en el proceso durante el procesamiento. Si hay varios mensajes en la cola para procesar un proceso / bloques en un proceso, el mensaje se pospone por algún tiempo. Al mismo tiempo, cada servicio puede realizar simultáneamente el trabajo en varias instancias de proceso.
Una situación puede surgir cuando varios mensajes vienen en un proceso tras otro para procesar bloques (ramas paralelas). Para reducir la cantidad de situaciones en las que tiene que posponer mensajes, WBS toma varios mensajes a la vez y los ejecuta uno tras otro, evitando el envío a la cola para la ejecución repetida debido al bloqueo del proceso.

Conversión


Después de la transición a un nuevo motor, surgió la pregunta, ¿qué hacer con las instancias de proceso existentes? La opción preferida era su conversión, para que continuaran trabajando en el nuevo motor. Las ventajas son obvias: solo admitimos un motor, los problemas de soporte del viejo motor desaparecen (ver arriba). Pero existía el riesgo de que no pudiéramos descubrir completamente cómo obtener los datos que necesitamos de las instancias de proceso serializadas. También hubo un retroceso: dar instancias existentes para finalizar en el motor antiguo y lanzar nuevos en uno nuevo. Las desventajas de esta opción se derivan de las ventajas de la anterior, además de que se necesitan recursos adicionales para girar ambos motores.
Para la conversión, necesitábamos tomar el estado anterior del proceso en formato WF y generar los estados de procesos y bloques. Escribimos una utilidad que tomó el estado serializado de una instancia de proceso en la base de datos, extrajo de ella una lista de bloques activos, resultados de ejecución para caras y prácticamente ejecutó el proceso. Como resultado, obtuvimos el estado de la instancia en el momento de la conversión.
Surgieron dificultades para deserializar adecuadamente los datos de instancia de proceso en WF. El estado de la instancia de proceso (instancia) de WF se almacena en la base de datos como xaml. No pudimos encontrar una descripción clara de la estructura de este xaml, tuvimos que ir empíricamente hasta el final. Analizamos los datos manualmente y obtuvimos la información que necesitábamos. Como parte de esta tarea, elaboramos otra opción: usar herramientas WF para deserializar el estado de la instancia e intentar obtener información de los objetos. Pero debido al hecho de que la estructura de tales objetos era muy compleja, abandonamos esta idea y nos decidimos por el análisis "manual" xaml.
Como resultado, la conversión fue exitosa, y todas las instancias de proceso comenzaron a ser procesadas por el nuevo motor.

Conclusión


Entonces, ¿qué nos dio el motor Workflow? En realidad, logramos vencer todos los problemas expresados ​​al comienzo del artículo:
  • El motor está escrito en .NET Core;
  • es un servicio de autohospedaje independiente de IIS;
  • Como operación de prueba, utilizamos activamente el nuevo motor en el sistema corporativo y logramos asegurarnos de que el análisis de errores tome mucho menos tiempo;
  • realizó pruebas de carga en Postgres, según datos preliminares, un montón de WPS + WBS puede hacer frente fácilmente a la carga de 5000 usuarios concurrentes;
  • y, por supuesto, como cualquier trabajo interesante, es una experiencia interesante.

Como beneficio adicional, obtuvimos un código claro y compatible que podemos adaptar a nosotros mismos.
El costo del motor resultó ser comparable con lo que tendríamos que gastar en la compra / adaptación de un producto de terceros. Por el momento, creemos que la decisión de desarrollar su propio motor resultó justificada.
También estamos esperando pruebas de carga para más de 10,000 usuarios simultáneos. ¿Quizás será necesaria alguna optimización, o tal vez despegará? ;-)
Recientemente lanzamos DirectumRX 3.2, que incluía el nuevo Workflow. Veamos cómo se mostrará el motor a los clientes.

Source: https://habr.com/ru/post/468803/


All Articles