Azure Durable Functions | Suborquestaciones

Azure Durable Functions es una gran extensión de las Azure Functions que nos permite generar «recetas» o definir procesos que involucren diferentes Azure Functions para llevar a cabo una tarea cuyo resultado conjunto no pueda ser resuelto por una de ellas debido a su complejidad. De esta forma, una Durable Function comienza con un «Orquestador» que definirá las reglas o el flujo que deben seguir en la actuación de las diferentes Azure Functions involucradas . Hasta aquí todo es relativamente sencillo pero, ¿qué ocurre cuando el proceso incluye a su vez subprocesos complejos? Es aquí donde aparecen las Suborquestaciones y os lo enseño en este artículo con código y en el vídeo incluído al final.

Contexto

Supongamos que tenemos un sistema donde necesitamos realizar un proceso complejo como por ejemplo, recoger datos de diferentes fuentes, transformarlos, almacenarlos y finalmente analizarlos para mostrar un resultado en base a cálculos con esos datos, algo así como haría un algoritmo de Machine Learning.

En principio, si fueran datos simples y de una única fuente, no tendríamos mayor problema pero, si se trata de una recopilación de datos de diferentes fuentes, pasando a través de APIs variopintas, transformarlos para normalizarlos de forma que los podamos tratar en nuestro proceso, almacenarlos en ficheros (excel, json,…) y en bases de datos (CosmosDB, SQL Server, Oracle,…) y finalmente analizarlos mediante nuestros propios algoritmos o mediante otros servicios externos, la cosa se complica muchísimo.

Vista la complejidad a la que podemos llegar, debemos empezar a dividir el proceso principal en subprocesos menos complejos que pueden requerir también de una orquestación, es decir, suborquestadores, que realizarán las tareas de cada uno de los subprocesos del orquestador principal y, si lo necesitaran, podrían a su vez realizar llamadas a otros suborquestadores de más bajo nivel como veremos en el ejemplo a continuación en el que he usado el contexto de Saint Seiya haciendo un repaso por las batallas de la Saga del Santuario, algo así como la primera temporada (Lo sé soy muy friky)

Orquestador Principal

Comencemos con el orquestador principal que será la receta a más alto nivel y que será donde iniciemos las llamadas a las suborquestaciones mediante el uso de context.CallSubOrchestratorAsync, que se corresponderán con las diferentes batallas o grupos de batallas:

  • Saga del Santuario (Nuestro Proceso completo)
    • El Torneo Galáctico (Batalla)
    • Fénix: los caballeros negros (Batalla)
    • Los Santos de plata: La lucha por la armadura de oro (Batalla)
    • Los Santos de Oro: La batalla de las doce casas (Grupo de Batallas)
        [FunctionName("SaintSeiyaOrchestrator")]
        public static async Task RunOrchestrator([OrchestrationTrigger] IDurableOrchestrationContext context, ILogger log)
        {
            if (!context.IsReplaying)
            {
                Logs.LogSaintMessage($"** COMIENZA LA SAGA DEL SANTUARIO ** : {context.InstanceId}");
            }
            
            await context.CallSubOrchestratorAsync("SingleBattleOrchestrator", "Torneo Galáctico");
            await context.CallSubOrchestratorAsync("SingleBattleOrchestrator", "Fénix: Los caballeros negros");
            await context.CallSubOrchestratorAsync("SingleBattleOrchestrator", "Los Santos de plata: La lucha por la armadura de oro");

            var multipleBattle = new MultipleBattle
            {
                Title = "Los Santos de oro: La batalla de las doce casas",
                Battles = new List<string> { "Aries", "Tauro", "Gemini", "Cáncer", "Leo", "Virgo", "Libra", "Escorpio", "Sagitario", "Capricornio", "Acuario", "Piscis", "Grand Pope" }
            };
            
            await context.CallSubOrchestratorAsync("MultipleBattleOrchestrator", multipleBattle);

            if (!context.IsReplaying)
            {
                Logs.LogSaintMessage($"** FINALIZADA LA SAGA DEL SANTUARIO ** : {context.InstanceId}");
            }
        }

Como podéis ver en el código, hacemos uso de context.CallSubOrchestratorAsync para realizar llamadas a los suborquestadores como son «SingleBattleOrchestrator» y «MultipleBattleOrchestrator» pasándoles además como parámetro el nombre de la batalla.

Este método tiene varios «overloads» que son los que siguen a continuación:

  • context.CallSubOrchestratorAsync (string FunctionName, string input)
  • context.CallSubOrchestratorAsync (string FunctionName, string instanceId, string input)
  • context.CallSubOrchestratorAsync (string FunctionName, object input)
  • context.CallSubOrchestratorAsync (string FunctionName, string instanceId, object input)

El parámetro input debe ser serializable o de lo contrario el proceso fallará.

Orquestador SingleBattleOrchestrator

El orquestador SingleBattleOrchestrator será muy simple y no contiene sino una llamada a una function que determinará el resultado de la batalla.

        [FunctionName("SingleBattleOrchestrator")]
        public static async Task<string> RunOrchestrator(
            [OrchestrationTrigger] IDurableOrchestrationContext context, ILogger log)
        {
            var battleTitle = context.GetInput<string>();
            var result = await context.CallActivityAsync<string>("SingleBattleOrchestrator_Battle", battleTitle);

            if (!context.IsReplaying)
            {
                Logs.LogSaintMessage($"{result}");            
            }
            return result;
        }

Como se puede observar en el código, transformamos la entrada en un string para poder mostrar por consola el paso en el que nos encontramos y realizamos una llamada a una function manejada por orquestador mediante context.CallActivityAsync.

Orquestador MultipleBattleOrchestrator

Este orquestador debe realizar un proceso más complejo con lo que va a necesitar de realizar llamadas a nuevos suborquestadores que, en este caso y por simplificar, se corresponderán con llamadas al SingleBattleOrchestrator para ejecutar el proceso de batallas simples para cada una de las 12 batallas asignadas a la variable «var multipleBattle» que definí en el orquestador principal. Así pues, el código de este orquestador es el que sigue.

        [FunctionName("MultipleBattleOrchestrator")]
        public static async Task RunOrchestrator(
            [OrchestrationTrigger] IDurableOrchestrationContext context)
        {
            var multipleBattle = context.GetInput<MultipleBattle>();
            if (!context.IsReplaying) {
                Logs.LogSaintMessage($"{multipleBattle.Title} stage has started!");
            }

            foreach (var battle in multipleBattle.Battles)
            {
                //var result = await context.CallActivityAsync<string>("MultipleBattleOrchestrator_Battle", battle);
                await context.CallSubOrchestratorAsync("SingleBattleOrchestrator", battle);
            }

            if(!context.IsReplaying)
            {
                Logs.LogSaintMessage($"{multipleBattle.Title} stage has ended!");
            }
        }

Se puede apreciar el código cómo obtenemos el objeto serializable del input que hemos pasado desde el orquestado principal, asignándoselo a una variable que quedará tipada como «MultipleBattle«. Posteriormente se recorre cada una de las 12 batallas y por cada una de ellas se realiza una llamada a al suborquestador

Resultado

El resultado es una ejecución orquestada mediante un patrón secuencial en el que se han realizado todas y cada una de las tareas que componían los subprocesos para así dar solución al proceso principal.

Ejemplo en vivo

Conclusion

Las suborquestaciones con Azure Durable Functions nos abren la posibilidad de realizar procesos muy complejos de una forma ordenada y fácilmente interpretable y nos permite además simplificar a alto nivel la visión del proceso completo.

Espero que os haya gustado el artículo.

Enjoy coding!