Flow Builder: Concurrencia y Versionamiento con Dapr¶
Este documento evalúa las capacidades técnicas de Dapr (Distributed Application Runtime) y Quarkus para resolver la arquitectura híbrida de un creador visual de flujos (Flow Builder) en DKV Pet Flows.
El foco se centra en dos fases críticas del ciclo de vida del flujo: el tiempo de diseño (Design-Time) y el tiempo de ejecución (Execution-Time), mitigando condiciones de carrera y gestionando versiones en vuelo ("In-Flight").
1. Gestión de Configuración (Design-Time)¶
Durante el tiempo de diseño, múltiples editores del equipo pueden intentar modificar el mismo flujo simultáneamente. Es imperativo asegurar la integridad estructural y evitar colisiones ("Race Conditions").
El Dapr Actor como Definition Manager¶
Patrón Arquitectónico: Utilizar un Dapr Actor (FlowDefinitionManager) como guardián exclusivo de la entidad FlowDefinition. El identificador del Actor será el ID único del flujo (ej. actorId = "abandoned-conversation-flow").
Evaluación de Capacidades¶
- Concurrencia Turn-Based (Single-Threaded): Los Dapr Actors procesan mensajes (invocaciones de métodos) en un modelo estricto de un-hilo-por-instancia. Si dos editores guardan cambios en el mismo milisegundo, la infraestructura Dapr encola las peticiones invocándolas secuencialmente por instancia de actor. Esto elimina nativamente el problema del bloqueo de concurrencia optimista/pesimista clásico en la base de datos (se alinea de forma nativa con el Patrón Guardián).
- Gestión Transaccional: El estado del Actor (
State) se almacena de forma atómica a través del componente Dapr State Store (ej. PostgreSQL o Redis configurado). Si la validación lógica del esquema JSON del Flow Builder falla durante el procesado Java/Quarkus, la transacción de estado se aborta, garantizando la integridad. - Versionado Semántico Automatizado: En el método
updateFlowDefinition(), el Actor puede gobernar el versionado. Al recibir un Publish/Deploy, el Actor incrementa automáticamente su versión activa global (ej. dev1av2), almacenando el historial de esquemas como propiedades inmutables dentro del State Store, e informando al resto del sistema vía Pub/Sub (flow-definition-updatedevent) para coordinar invalidación de cachés.
[!SUCCESS] Decisión El modelo Dapr Actor resuelve de forma elegante y nativa 100% de los requerimientos de edición colaborativa sin necesidad de implementar sistemas de Lock distribuido por Redis manualmente en Quarkus o DB Locks complejos.
2. Versión Runtime e In-Flight Workflows (Execution-Time)¶
Contexto: Cuando el flujo arranca tras un evento del dominio, se instancia un Dapr Workflow que usa internamente el Durable Task Framework. Los Dapr Workflows utilizan el patrón Event Sourcing reconstruyendo el estado del código a base de re-evaluar la historia registrada.
El Problema Planteado:
Un usuario (Encounter) entra en Flujo V1 y llega a una actividad o instrucción de espera como createTimer(24h), lo cual suspende la instancia del workflow en backend para no consumir RAM. Durante esa suspensión de horas, un editor accede al visual Flow Builder y aprueba y publica una "versión V2" del flujo cambiando el timer de inactividad de "24 horas" a "5 días".
Restricciones del Durable Task Framework (Dapr Workflows)¶
- Determinismo: No se puede cambiar dinámicamente la definición abstracta inyectada o el código duro para flujos ya iniciados si modifica el orden de ejecución. Si se reescribe el código (y la topología cambia), Dapr lanzará un
NonDeterministicWorkflowExceptiondurante el subsiguiente Replay cuando intente recuperar el estado. - Inmutabilidad In-Flight por Defecto: Las instancias en vuelo (In-Flight instances) pertenecen incondicionalmente a la Firma/Esquema de Workflow con la que fueron creadas. Continuarán su ejecución basándose en la configuración estática/estado inyectado inicial, a menos que sean diseñadas de forma reactiva para escuchar eventos de re-configuración.
Patrones Arquitectónicos Evaluados para "Update In-Flight"¶
Opción A: Actividades de proxy diferidas (Late-Binding Config)¶
El workflow llama a una actividad getConfig() justo instantes antes de planificar la pausa para saber cuánto tiempo estar durmiendo.
* Mecánica: int hours = ctx.callActivity("GetTimerDurationActivity", ...) -> ctx.createTimer(Duration.ofHours(hours)).
* Problema para el caso de uso: El workflow ya ha enviado la orden de timer al motor asíncrono de Dapr. Si cambiamos la config en Design-Time durante la pausa, el workflow no lo detectará jamás porque el timeout ya está impreso a fuego dependiente del cronómetro interno; la actividad no se volverá a evaluar. Solo funciona para etapas venideras pero es ciego a modificaciones sobre los temporizadores actualmente in-flight.
Opción B: Cancel & Restart (Borrón y Cuenta Nueva)¶
Terminar abruptamente la instancia V1 de los usuarios implicados y arrancar para ellos Instancias V2.
* Mecánica: Dapr provee un endpoint para emitir terminaciones anticipadas: daprClient.terminateWorkflow(instanceId). Quarkus recogería este kill, leería el Business Context antiguo del store, y arrancaría en limpio una V2 pasándole contexto.
* Veredicto: Patrón altamente destructor. Provoca pérdida de fidelidad en la observabilidad visual y telemetría de Zipkin, causando registros duplicados y destrozando el historial ininterrumpido que provee durable tasks. Difícil de calcular "el tiempo restante in-flight" migrándolo.
Opción C: External Signals (External Event Re-Configuration) — Recomendada¶
En lugar de un estático y sordo ctx.createTimer(24h), el flujo encapsula la detención esperando una "carrera" (anyOf) entre el Cronómetro y una [External Signal / Evento].
// Patrón de Conciliación de Tiempos
public void run(WorkflowContext ctx) {
Duration remainingSleep = ctx.getInput(Duration.class); // Inicialmente 24h
while (!remainingSleep.isNegative() && !remainingSleep.isZero()) {
Task<Void> timerTask = ctx.createTimer(remainingSleep);
Task<String> configUpdateSignal = ctx.waitForExternalEvent("ConfigUpdated", String.class);
Task<?> winner = ctx.anyOf(timerTask, configUpdateSignal);
if (winner == configUpdateSignal) {
// El usuario publicó la V2. El FlowDefinition Actor dispara este event
// despertando intencionalmente al Workflow para recalcular.
Duration newTotalDuration = ctx.callActivity(...); // Lee nueva DB config
remainingSleep = computeRemaining(newTotalDuration, timeElapsedSinceStart);
} else {
// Se completó el timer orgánico/actualizado. Despierta y sale del bucle.
break;
}
}
}
FlowDefinition Actor solo iteraría las IDs activas del flujo base para evocar un daprClient.raiseEvent("ConfigUpdated") a sus suscripciones.
[!TIP] "Versiones Estancas" como alternativa trivial A menudo la regla de oro minimalista en orquestadores DAG es sencillamente: Dejar a la V1 morir su muerte natural como V1. Todo nuevo "Encounter" ingresa orgánicamente siendo V2 y los 5 usuarios siguen suspendidos como V1 a 24 horas. Implementar 'External Signals' justifica su uso sólo cuando el negocio exige coercitivamente que una regla "de compliance/urgencia" afecte retroactivamente a los vuelos detenidos.
3. Modelado de los Comandos Globales: "Start" y "Stop" del Flujo para MVP¶
Desde el Flow Builder, en cualquier instante el editor puede presionar un botón rojo de STOP y paralizar enteramente el procesamiento para un workflow, o reanudarlo (START).
Atención: Esto incide en cómo se absorben webhooks venideros de netcomp.
Comando Pánico: "Stop" (Pausa del Flujo)¶
-
Pausado del Pipeline In-Flight (SDK API): El SDK de Dapr posee operaciones nativas robustas para freezar instancias:
/pausey/resume.daprClient.pauseWorkflow(instanceId, workflowComponent);Esto detiene en seco a los hilos de trabajador que avanzan en Actividades actuales del "V1", interrumpiéndolo con integridad a nivel orquestador Dapr. El Actor, tras el comando, itera la vista activa y lanza la orden de Pause a las instancias vivas. -
Rechazo (o Buffer) en el Gateway Periférico AMQP: Netcomp (
dkv_telemed_util.erl) continuará disparandocreate_event/na ciegas. El entrypoint MQ en Quarkus (e.g.,EncounterEventConsumer.java), previo a emitir el clásicodaprClient.scheduleNewWorkflow(), invoca al Actor Guardián para extraer vía ultra-rápida (State cache) si el Flow en cuestión estáis_active. - Flujo Apagado/Pausado: Si está inactivo, Quarkus efectúa un
NACK(negative-acknowledge requeue o Drop-and-DLQ al RabbitMQ). Al ser un MVP, un drop seguro o un Dead-Lettering local soluciona la presión del ecosistema de manera inofensiva previniendo iniciar flujos inválidos.
Reanudación de la Cascada: "Start" (Reanudación del Flujo)¶
- El visual UI acciona al Actor a cambiar su flag interno a
ACTIVE(estado normal). - Acto seguido, los eventos provenientes del gateway RabbitMQ son admitidos lícitamente fluyendo hasta
scheduleNewWorkflow. - Inmediatamente el Actor emite de forma masiva sobre la pool in-flight las directrices
daprClient.resumeWorkflow(instanceId, ...), levantando su estado suspendido a "Running" nuevamente.
Evaluación Final¶
Dapr está estructuralmente dotado de facto de capacidades enterprise-grade para un engine de Flow-Builder Visual. Combina control predictivo a través del estricto cerrojo transaccional lock-free de los Dapr Actors, con un abanico funcional pragmático en los Durable Workflows capaz de someter In-Flights a actualizaciones Live y pausas holísticas inofensivas.
Evidencia de Investigación (NotebookLM)¶
Contraste validado utilizando el pipeline de IA @/notebooklm-ops contra el cuaderno de arquitectura "Quarkus + Dapr: Deep Research".
- Dapr Actors vs Alternativas Clásicas: La investigación ratifica que el uso de Dapr Actors evita activamente la pesadilla de concurrencia y despliegue de bloqueos (lockings) manuales o particionamiento (cluster sharding) que se requería en arquitecturas previas de la compañía basadas en Spring + Pekko.
- Workflow as Code e In-Flight: Las fuentes confirman que el paradigma "Workflow as Code" se basa en compilación fuerte, requiriendo Event Sourcing estricto. La recomendación bibliográfica oficial es unánime respecto a mitigar terminaciones abruptas en favor de
waitForExternalEvent(Signals) para un manejo dinámico y reactivo, o complementar flujos de larga duración con la APIcontinue-as-newpara reiniciar el ciclo vital de ser necesario.