Una nueva mirada a Azure Durable Functions

El arquitecto principal de Zone, Andy Butland, examina cómo ha madurado el marco de funciones duraderas en los últimos años …

Hace un par de años tuve la oportunidad de hablar, escribir y trabajar con lo que entonces era una nueva tecnología de Azure sin servidor, Durable Functions, basada en la parte superior del marco de Azure Functions con el propósito de manejar procesos de larga duración o de múltiples etapas Tareas. Como parte de la investigación de la tecnología, construí un par de aplicaciones de muestra utilizando funciones duraderas y el marco de funciones estándar por sí solo, y comparé y contrasté los resultados.

Lo que es obvio es que con el enfoque de funciones duraderas, gran parte del “código de plomería” se abstrae de usted como desarrollador, lo que le permite concentrarse en la lógica comercial del flujo de trabajo que desea implementar.

Puede ver en un diagrama de componentes “antes y después” que muchos de los componentes de almacenamiento y cola intermedios necesarios ya no son una preocupación inicial, y podemos confiar más en el marco en sí para las tareas de ordenar datos en el funciona como parámetros y vuelve a aparecer como resultados.

También obtiene el agradable beneficio de poder aplicar el principio de responsabilidad única a sus funciones, uniendo lo que puede ser un flujo de trabajo complejo a partir de una serie de componentes individuales, cada uno centrado en su propia tarea específica.

Si está interesado en leer más sobre esta comparación entre las funciones de Azure y el nuevo enfoque de funciones duraderas, consulte mi artículo anterior.

Un nuevo problema que resolver

En un proyecto reciente, tuvimos un problema con el flujo de trabajo de carga de archivos. A través de una aplicación de back-office, permitimos al usuario exportar un conjunto de datos en formato Excel, modificarlo y volver a cargar el archivo para actualizar por lotes sus cambios. Si bien esto funcionó, debido a que nuestra implementación inicial fue hacer esto dentro de un ciclo de solicitud / respuesta web único y sincrónico, tuvimos algunos problemas con las cargas de archivos grandes. Detrás de escena, la aplicación web estaba haciendo una llamada a una API REST, cuyo procesamiento actualizaba los registros uno a la vez en una base de datos, y este era nuestro cuello de botella.

Si bien podríamos tratar los síntomas aquí con un mayor tiempo de espera, y quizás un agrupamiento interno de las actualizaciones, estaba claro que el problema principal aquí realmente era el intento de hacer esto dentro de un ciclo de solicitud / respuesta sincrónico. Un mejor enfoque, al menos una vez que hayamos detectado que el archivo subido era mayor que un tamaño en particular, sería cambiar al procesamiento asíncrono.

En su lugar, implementaríamos la funcionalidad de la aplicación web como esta:

El propio flujo de trabajo debe:

Finalmente, de vuelta en la aplicación web, tenemos:

Estos flujos de datos se ilustran en el siguiente diagrama:

Dado que la solución ya estaba alojada en Azure, teníamos acceso a las instalaciones necesarias de almacenamiento en cola y blob, y para el procesamiento asincrónico en sí, teníamos una razón para volver a recurrir a funciones duraderas. En el resto de este artículo, compartiré algunos detalles y ejemplos de código de los aspectos que encontré “nuevos y mejorados” desde mi última oportunidad de trabajar con la tecnología.

La “abstracción con fugas” está tapada

Cuando trabajaba con una versión anterior de funciones duraderas, un problema que encontré fue que, aunque los componentes de almacenamiento intermedios se abstraen, por supuesto, todavía existen. En ese momento, solo se usaban colas para pasar datos entre funciones, que tienen un límite de tamaño de mensaje de 64 KB. Por lo tanto, si los datos que se transmiten exceden ese tamaño, se producirán problemas. Podría serializar y comprimir datos pero luego perder la naturaleza fuertemente tipada de la mensajería. E incluso entonces, podría superar el límite.

Lo que encontré realmente bueno esta vez fue que esta abstracción del componente de almacenamiento realmente se mantiene, con el marco usando la compresión sin problemas y cambiando de colas a otras formas de almacenamiento según lo requiera el tamaño de los datos. Esto significa que no hay necesidad de serialización y compresión, y realmente puede olvidarse de las partes de plomería del flujo de trabajo.

El flujo general de un flujo de trabajo de orquestación duradero es:

En los ejemplos de código a continuación, puede ver un extracto de la implementación, donde se activa una orquestación, pasando un parámetro fuertemente tipado.

Inyección de dependencia

Otra característica ahora completamente implementada con las funciones de Azure es la capacidad de usar el patrón de inyección de dependencia. Esto es algo que usamos mucho con .Net cuando trabajamos en otros contextos, como aplicaciones web MVC o API, pero anteriormente esto no se lograba fácilmente con las funciones de Azure. Eran esencialmente clases estáticas y, por lo tanto, los medios típicos de inyección de dependencias del constructor no estaban disponibles.

Para solucionar esto antes, usaría una especie de “DI del pobre”, donde instancias componentes en la función y luego los pasas a una clase separada a través de su constructor, donde la mayoría de los el trabajo sucedió. Esto fue útil para respaldar las pruebas hasta cierto punto, ya que se podía validar el comportamiento de la clase usando dependencias simuladas, pero no fue particularmente elegante.

Sin embargo, a partir de Azure Functions 2.0, el patrón de inyección de dependencia ahora es totalmente compatible.

El registro de dependencias se realiza en una clase especial decorada de tal forma que se ejecutará al inicio. Aquí puede ver que estamos registrando un HttpClient con nombre, así como un servicio para acceder al almacenamiento de blobs, utilizando variables de entorno para la configuración.

Luego, en las funciones de actividad mismas, podemos usar la inyección del constructor para obtener una instancia concreta en tiempo de ejecución. Primero para HttpClient :

Y luego para el servicio BLOB:

Prueba unitaria

Otro descubrimiento agradable al observar las funciones duraderas en 2020 es que ahora son completamente probables por unidad. Como se señaló anteriormente, buscaría extraer el código bajo prueba en una clase separada, instanciada desde dentro de la función en tiempo de ejecución, y probar eso. Que cubría la mayoría de las cosas, pero ahora podemos probar cada uno de los tres tipos de función en un flujo de trabajo de funciones duraderas directamente.

En primer lugar, la función de activación. Por lo general, este no hace mucho, pero al menos podemos verificar que estamos llamando a la orquestación adecuada y brindando los datos de entrada correctos:

Luego la orquestación. La responsabilidad de la función de orquestación en un flujo de trabajo es no hacer ningún trabajo con los servicios externos en sí; de hecho, hay restricciones de código que debe cumplir para asegurarse de que no lo haga, sino que está involucrado con la llamada a las funciones de actividad apropiadas, pasando la necesaria introducir datos y cotejar los resultados. Podemos escribir una prueba para asegurarnos de que está haciendo esto, como en este ejemplo simplificado:

Finalmente, la actividad funciona por sí misma. De manera similar, podemos burlarnos de la clase de contexto de funciones duraderas y verificar que funcione como debería:

Lógica de reintento

Una característica del uso de funciones de Azure únicas activadas por cola es el comportamiento de reintento disponible, mediante el cual si la función arroja una excepción, el mensaje se devolverá a la cola y se volverá a procesar para volver a procesarlo, después de un retraso configurable y un número máximo de intentos. Perdemos esa facilidad con funciones duraderas, ya que la función de activación que comienza la orquestación se completa una vez que sale con éxito y el mensaje se elimina. Una excepción dentro de las funciones de orquestación o actividad será demasiado tarde para evitar que eso suceda.

Sin embargo, tenemos cierto control sobre la lógica de reintento dentro del propio flujo de trabajo de funciones duraderas.

En primer lugar, podemos llamar a funciones de actividad con comportamiento de reintento, utilizando un objeto que indica cuántas veces nos gustaría reintentar y con qué demora:

Y podemos usar la lógica try / catch , con excepciones que se propagan a la función de orquestación, donde, si corresponde, se puede detectar la FunctionFailedException y se pueden realizar acciones compensatorias hecho.

Para restaurar el comportamiento de una única función de Azure, donde el mensaje se vuelve a colocar en la cola para su procesamiento nuevamente, sería necesario realizar una acción específica para colocar una nueva copia del mensaje en la cola. Si el mensaje sigue fallando, no querrá seguir procesándolo indefinidamente; normalmente se trasladaría después de algunos intentos a una “cola de veneno” para su posterior manipulación, a menudo manual. Sin embargo, por lo que puedo ver, el recuento de eliminación de cola de un mensaje no se puede establecer, solo leer, por lo que se necesitarían algunos medios personalizados para rastrear esto a través del contenido del mensaje o un encabezado.

Resumen

El uso de funciones duraderas ha demostrado ser una herramienta valiosa para resolver este tipo de problema, en el que queremos romper trozos de funcionalidad de una aplicación web monolítica y manejarlos de forma asincrónica a través de procesos en segundo plano. Es agradable ver qué tan bien ha madurado el marco en los últimos años y cómo podemos usar las mejores prácticas a las que estamos acostumbrados en otras áreas al programar en este paradigma todavía relativamente nuevo de “sin servidor”.