Durable Functions
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:
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 FunctionIDurableEntityClient
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);
}