SetValue.NETSetValue.NET

Durable Functions

May 30, 2020

Las Azure Functions son una de las soluciones más útiles que existen para orquestar operaciones y resolver problemas complejos sin necesidad de preocuparse por la infraestructura que la soporte. Estas aplicaciones son pequeños fragmentos de código que son desencadenados por distintos tipos de eventos, en definitiva son una solución de tipo Event-driven que se ejecutan cuando se desencadena un trigger.

Estos trigger pueden ser de varios tipos, unos ejemplos:

  • HTTP
  • Timer
  • Queue
  • Blob

Bien, después de esta breve intro, ¿qué ocurre cuando necesitamos ejecutar una operación long-running que es desencadenada desde un tercero? Por ilustrar el ejemplo, imaginamos que tenemos un job que se desencadena todas las noches y que orquesta varias operaciones:

Result A
Result A
Result A+B+C
Daily Job
Function A
Long-Running Process B
Long-Running Process C
Function D
Waiting Completed
Report Results

En el gráfico anterior tenemos un proceso diario que desencadena una Function A y con el resultado, llama a los procesos B y C, estos procesos son de larga duración, y debemos esperar a que se completen, una vez finalizados, el resultado agregado de los procesos A, B y C se le pasará a la Function D.

Esta aproximación, que podría ser fácilmente una integración nocturna con una compañía con la que intercambias información diariamente, puede no ser tan ágil como para que las llamadas a los procesos B y C no acaben en timeout.

Para resolver este escenario de ejemplo podemos utilizar las Durable Functions.

¿Qué son las Durable Function?

Es una extensión de las Azure Function que permite crear funciones con gestión de estados. Esto quiere decir que la Azure Function controlará el estado de la misma, realizará checkpoints y se reiniciará si es preciso.

Tipos

Actualmente existen cuatro tipos distintos de Durable Functions.

  • Orchestrator. Para articular u orquestar grupos de operaciones.
  • Activity. Es una operación atómica en una orquestación.
  • Entity. Gestión de estados de una entidad.
  • Client. Para iniciar o llamar a los Orchestrator o Entity.

Orchestrator functions

Este tipo de funciones definen cómo y en qué orden se ejecutan distintas acciones. Tienen las siguientes características:

  • Definen workflows usando código procedural sin necesidad de esquemas o diseñadores.
  • Pueden llamar a otras Durable Functions de forma síncrona o asíncrona.
  • Son duraderas y confiables, el progreso se gestiona automáticamente, el estado local no se pierde aunque el proceso se recicle o se reinicie la máquina virtual.
  • Pueden ser procesos de largo recorrido (long-running), el tiempo de vida de la orquestación podría ser segundos, minutos, días o no acabar nunca.

Son desencadenadas por un trigger específico para orquestadores IDurableOrchestrationContext.

[FunctionName("GetCurrentDate")]
public static async Task<DateTime> Run([OrchestrationTrigger] IDurableOrchestrationContext context)
{
    string timeZone = context.GetInput<string>();
    string result = await context.CallActivityAsync<string>("GetCurrentDateActivity", timeZone);
    return result;
}

Importante: Este tipo de Function se escribe de forma determinista, es decir, siempre que se ejecute deberá producir el mismo resultado.

Activity functions

Son la unidad básica de trabajo en una orquestación, las actividades o tareas que serán gestionadas por un orquestador. Cada actividad es una función independiente que se puede ejecutar en serie o en paralelo. Y no tienen ninguna limitación de tipos de trabajos que pueden realizar.

A partir de un trigger, las actividades pueden recibir por parámetro el contexto. A este desencadenador se le puede hacer un binding con un objeto serializable que pueda llevar diversos inputs a la function. En las actividades sólo es posible pasar un único valor, por lo que si es necesario pasar varios valores se pueden utilizar tipos complejos, arrays o tuplas.

//Tipo de parámetro por defecto (IDurableActivityContext)
[FunctionName("GetCurrentDateActivity")]
public static DateTime GetCurrentDateActivity([ActivityTrigger] IDurableActivityContext currentDateContext)
{
    string timeZone = currentDateContext.GetInput<string>();
    return timeZone == "UTC" 
         ? DateTime.UtcNow 
         : TimeZoneInfo.ConvertTimeFromUtc( DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById(timeZone));
}

//Tipo de parámetro serializable
[FunctionName("GetCurrentDateActivity")]
public static DateTime GetCurrentDateActivity([ActivityTrigger] string timeZone)
{
    return timeZone == "UTC" 
         ? DateTime.UtcNow 
         : TimeZoneInfo.ConvertTimeFromUtc( DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById(timeZone));
}

Entity functions

Son funciones que administran el estado de una entidad de forma explícita. Tienen un tipo de trigger específico, y se comportan de una forma similar a pequeños servicios que se comunican a través de mensajes. Cuando se ejecutan, pueden actualizar el estado interno de la propia entidad.

public class CounterFunction
{
    [FunctionName("Counter")]
    public static void Counter([EntityTrigger] IDurableEntityContext ctx)
    {
        switch (ctx.OperationName.ToLowerInvariant())
        {
            case "add":
                ctx.SetState(ctx.GetState<int>() + ctx.GetInput<int>());
                break;
            case "reset":
                ctx.SetState(0);
                break;
            case "get":
                ctx.Return(ctx.GetState<int>());
                break;
        }
    }
}

[JsonObject(MemberSerialization.OptIn)]
public class Counter
{
    [JsonProperty("value")]
    public int CurrentValue { get; set; }

    public void Add(int amount) => this.CurrentValue += amount;

    public void Reset() => this.CurrentValue = 0;

    public int Get() => this.CurrentValue;

    [FunctionName(nameof(Counter))]
    public static Task Run([EntityTrigger] IDurableEntityContext ctx) => ctx.DispatchAsync<Counter>();
}

Client functions

Las Orchestrator o las Entity Functions se desencadenan con unos trigger específicos que reaccionan a los mensajes que se encolan. Para poder enviar estos mensajes podemos utilizar estos dos binding de DurableClient:

  • IDurableOrchestrationClient para las Orchestrator Function
  • IDurableEntityClient para las Entity Function

Este Client, además de permitir desencadenar las Function, nos permite realizar operaciones sobre la misma, como puede ser consultar, finalizar o generar algún tipo de evento.

// Orchestrator
[FunctionName("QueueStart")]
public static Task Run([QueueTrigger("durable-function-trigger")] string input, [DurableClient] IDurableOrchestrationClient starter)
{
    // Orchestration input comes from the queue message content.
    return starter.StartNewAsync("HelloWorld", input);
}

//Entity
[FunctionName("AddFromQueue")]
public static Task Run([QueueTrigger("durable-function-trigger")] string input, [DurableClient] IDurableEntityClient client)
{
    // Entity operation input comes from the queue message content.
    var entityId = new EntityId(nameof(Counter), "myCounter");
    int amount = int.Parse(input);
    return client.SignalEntityAsync(entityId, "Add", amount);
}
Buy Me A Coffee