Modelo de actores con Akka.Net – I

Bienvenidos todos de nuevo!! En esta ocasión os traigo una tecnología que esta cogiendo peso últimamente tanto en el mundo Front como Backend, y es nada menos que Akka.NET. El fuerte de ésta tecnología y lo que la está haciendo popular es la capacidad de poder construir aplicaciones distribuidas y concurrentes de una forma sencilla y fácilmente gestionable.

Akka.NET no es más que una herramienta que nos da la posibilidad de construir éste tipo de aplicaciones, trabajando bajo el framework .NET como su propio nombre indica. He de destacar en éste punto que la plataforma Akka existe de forma independiente a la plataforma .NET, también existe una versión que trabaja bajo Java. Yo en éste caso os lo mostraré bajo el framework .NET ya que es sobre el que más soltura tengo desarrollando.

Antes de comenzar realmente con el contenido recordar que realizaremos una aplicación de ejemplo utilizando Akka.NET explicando todo el potencial de ésta herramienta, así que si queréis ver el código resultante ya sabéis cual es el repositorio de GitHub (Proyecto FirstAkkaApp). Dicho esto comenzamos!!

Siguientes publicaciones:

Aplicaciones Reactivas

En éste mundo de alta competitividad en el ámbito del Software, todos los clientes quieren la mejor aplicación, aquella que tarda lo menos posible en responder a las acciones del usuario, aquella que en caso de fallo no se produce un caos total y aquella que se adapta a los cambios de forma prácticamente directa. Todos estos mandamientos que acabo de redactar son los que se encuentran recogidos en el Manifiesto Reactivo (De forma muy resumida), donde se específica claramente lo que el conjunto de empresas dedicado al desarrollo de software ha ido vislumbrando con el paso de los años qué es lo que quiere realmente el usuario cuando utiliza una aplicación.

A fin de cuentas los 4 puntos clave del susodicho manifiesto, y los cuales afirman que una aplicación es reactiva son:

  • Responsivos: El sistema responde lo antes posible a las acciones del usuario.
  • Resilientes: El sistema continua siendo responsivo incluso en caso de fallos.
  • Elásticos: El sistema sigue siendo responsivo incluso en situaciones de carga.
  • Orientados a Mensajes: Se apoyan de los beneficios que aportan los sistemas basados en paso de mensajes, ventajas como bajo acoplamiento, aislamiento y transparencia de ubicación.

Todo ello nos lleva a definir el concepto de programación reactiva, que es nada más y nada menos que código dirigido por la disponibilidad de los datos. Es decir, nuestro código reaccionará en tanto que los datos para los que dicho código ha sido erigido se encuentren disponibles en el sistema, todo lo contrario a un enfoque de Polling, en donde ante la incapacidad de reaccionar al instante ante nuevos cambios, el sistema elabora un proceso de consulta continua en pos de detectar dichos cambios, con la pega que ello conlleva.

Dicho esto usando Akka.Net nos serviremos de la programación reactiva, y no solo eso, seremos capaces de procesar una cantidad considerable de datos (Streaming) y de generar una respuesta acorde al usuario de manera casi instantánea. De hecho no existe otro ejemplo más claro en el uso de Akka.Net que no sea éste, ya que si tu aplicación o sistema no necesita de procesamiento concurrente para nada necesitas de ésta tecnología.

Modelo de Actores

Akka.Net sigue un modelo de actores, y cuando hablamos de un actor nos referimos a un ente que se encarga de realizar una tarea de forma independiente incluso a nivel de datos y de forma paralela a cualquier otro actor que existiese en el sistema. Visto desde un fin mucho más técnico un actor sigue un concepto muy parecido a lo que sería un hilo de procesamiento, cada actor podría ser un hilo que trabaja de forma independiente con su propio ámbito de datos que no es tratado por ningún otro actor, y además son capaces de colaborar entre sí (Y están obligados a ello) mediante el uso de mensajes. Obviamente los actores no son hilos per se, si no que internamente tratan los recursos disponibles en el sistema de forma optimizada y sin la necesidad de tener que tratarlos y mantenerlos como los hilos en memoria.

En un nivel más conceptual nos podríamos imaginar un actor como una casa. Todas las casas cuentan con un buzón de correo, que será donde los demás actores o entes se comunicarán con los habitantes de la casa para que estos realicen tareas, cada casa dispone además de una dirección y un número para que puedan comunicarse contigo, es decir, puedan identificarte. El funcionamiento en sí sería, una casa si no tiene cartas en su buzón no realiza ninguna tarea, así en cuanto reciba una carta o cartas, estas las leerá y atenderá a las necesidades en ella escritas de forma ordenada, y como en todas las casas cualquier persona externa a ellas no puede manejar la información o el contenido de los que no son propietarios.

En ésta analogía un actor contiene un buzón de entrada donde encolará los mensajes a procesar en orden. Cada actor dispone además de una URL única con la que podrán identificarlo (Akka se encarga de manejar esta URL), y cada actor puede almacenar su estado interno en la memoria del sistema.

Otro concepto de los actores que vislumbramos en éste punto es el tipo del actor, ya que los actores pueden ser distribuidos mediante un tipo, con lo que podríamos crear varios actores del mismo tipo para trabajar de manera concurrente.

Como vemos en la imagen, estos actores representan a usuarios interactuando con un carrito de la compra (Shopping Cart) de una tienda online. Aunque el tipo del actor sea el mismo cada uno representa una instantánea diferente para cada usuario que trabaja en el sistema de forma paralela.

Es por esto por lo que podríamos tener miles de actores trabajando en la aplicación, tantos como usuarios concurrentes se encuentren trabajando con nuestro sistema. Y esto no supondrá un exceso de carga te lo aseguro, los actores nacen para éste mismo proposito.

Así como podríamos tener varios actores en funcionamiento, recordemos que antes comentaba que los actores pueden contener estado interno, por lo que, nada nos impediría disponer de un usuario que aún continua añadiendo productos a su carrito de la compra, manteniendo el estado del actor en ‘Shopping, por ejemplo. Mientras que otro cliente ya ha finalizado la elección del catálogo y se dispone a pagar por los productos del pedido, por lo que el actor se podría encontrar en un estado como ‘Purchase Complete. Con esto estoy ilustrando que podríamos trabajar con actores como máquinas de estados en el tratamiento interno de su lógica, cosa que nos ayudará y simplificará la complejidad subyacente que tengamos que implementar.

Otra de las cualidades de los actores es la capacidad de poder generar actores hijos a partir de otro actor. Permitiéndonos así la generación de jerarquías de actores, con las que poder delegar responsabilidades en funcionalidades críticas de nuestro sistema y que ésta no colisione con cualquier otra operativa del resto de la aplicación, es decir, podríamos tener encapsuladas las partes críticas del código.

Como vemos en la imagen podríamos montar un sistema de actores como el anterior, donde en el ejemplo imaginemos que vamos a construir vehículos y para ello creamos un actor padre que será el Factory Actor, encargado de supervisar y delegar todo el trabajo de construcción a los actores hijos que parten de él, es decir, los Car Builder Actors, cuando al actor padre le llega un mensaje de construcción de un nuevo coche, éste lo delegará en alguno de sus actores hijos, y estos ha su vez delegarán la construcción de las distintas piezas del coche a los actores Wheel Builder Actor, Door Builder Actor… etc. Con ésto tan solo espero que se entienda el concepto de división del trabajo en unidades lo más pequeñas posibles o de carácter crítico, que es donde realmente radica el potencial en el uso de un sistema de actores como Akka.Net.

Antes de ponernos a implementar nuestra primera aplicación con actores, falta por destacar una de las cualidades clave del sistema de actores, y es el uso de Router Actors. Este tipo de actor nos permite tratar a un conjunto de actores como si fuese uno solo, posibilitando así la distribución de la carga del trabajo entre los distintos actores, donde por supuesto, el Router Actor será el encargado de dirimir quien realiza las tareas del conjunto de actores en todo momento. Por lo tanto, este tipo de actor sería el punto de entrada a nuestro sistema de actores.

Siguiendo con el mismo ejemplo de la factoría de vehículos como sistema de actores, el Factory Actor en nuestro caso sería el Router Actor, encargado de distribuir el trabajo entre los distintos actores del tipo Car Builder Actor. Más adelante en ésta serie de artículos veremos que existen distintas estrategias a la hora de dividir la carga de trabajo entre los actores por parte del Router.

Con ésto terminamos ésta pequeña introducción a algunas de las propiedades clave en el uso de un modelo de actores, que como modo de resumen volvemos a enumerarlas:

  • Dirección única de cada actor
  • Procesamiento de mensajes en orden
  • Cada actor posee un tipo
  • Los actores contienen estado
  • Permiten distribución jerárquica
  • uso de actores Routers

Una vez repasado lo básico del uso de actores podemos pasar a implementar nuestra primera aplicación con éste sistema. Así que, comenzamos!!

Primera aplicación con Akka .Net

Pasamos a crear nuestra primera aplicación usando Akka.Net, así que lo primero que vamos a hacer es crear una aplicación de consola .Net Core 3.1 en mi caso. Una vez creada lo primero será en nuestro proyecto bajarnos el paquete Nuget de Akka.Net, desde el propio administrador de paquetes que ya posee el Visual Studio 2019.

Como vemos nos aparece la versión 1.4.3 de Akka en mi caso, que es compatible con la versión de .Net Core 3.1 que dispongo, así que no tendré ningún problema de versionado. Recordemos antes de comenzar que el modelo de actores está diseñado para abordar la concurrencia desde una perspectiva más sencilla, abstrayendo la posible complejidad asociada con el uso de hilos, semáforos, monitores … etc.

Dicho esto comenzamos a definir nuestro primer actor:

using Akka.Actor;
using System;
using System.Collections.Generic;
using System.Text;

namespace FirstAkkaApp.Actors
{
    public class SmartPhoneActor : UntypedActor
    {
        protected override void OnReceive(object message)
        {
            Console.WriteLine($"SmartPhoneActor Receive a message: {message}");
        }
    }
}

Esta sería la forma más simple de definir un actor, simplemente heredamos de la clase UntypedActor, incluyendo la referencia al paquete Nuget de Akka que acabamos de añadir, e implementamos el método heredado de dicha clase OnReceive(object message), que será el método que capturará los mensajes enviados a dicho actor y por lo tanto cabe destacar en éste punto que las acciones realizadas en éste método son Thread-Safe. Simplemente con estas pocas líneas de código acabamos de crear un actor, que de momento tan solo mostrará por consola el mensaje recibido.

En el ejemplo que estoy desarrollando vamos a simular que nuestros actores son SmartPhones, los cuales serán capaces tanto de recibir como de enviar mensajes a otros SmartPhones. Y ahora para poder trabajar con el actor que acabamos de definir tendríamos que introducir el concepto de el Sistema de Actores.

Si para nosotros un actor es un SmartPhone, el sistema de actores que lo representa entonces sería la compañía de telefonía que nos suministra el servicio de mensajería en nuestros dispositivos. Básicamente representa el ente donde todos los actores son desplegados y administrados, ya que como el sistema de telefonía necesita de una central de administración y comunicación los actores actúan de una forma similar. Sin entrar demasiado en detalle las tareas del sistema de actores y que por ende son necesarias:

  • Planificación: Así como los sistemas multithread necesitan de una gestión del procesamiento para distribuir el trabajo y evitar los temidos DeadLocks. El sistema de actores sería el encargado en éste caso de realizar la misma tarea en los actores que sean desplegados en él.
  • Enrutamiento de mensajes: Es el encargado de hacer llegar los mensajes a los actores, ya se encuentren en diferentes localizaciones como hilos separados, otros sistemas remotos e incluso en un cluster.
  • Supervisión: También es el encargado de detectar, como último nivel, cuando los actores se encuentran en una situación de error, es decir, si un actor falla es el encargado de iniciar su recuperación.

Estas serían algunas de las tareas clave de las que se encarga de administrar el sistema de actores. He de añadir además que lo común es disponer de un sólo sistema de actores por aplicación. Proseguimos con la creación de un sistema de actores, desplegar el actor de ejemplo SmartPhoneActor y enviar un mensaje al mismo:

using Akka.Actor;
using FirstAkkaApp.Actors;
using System;

namespace FirstAkkaApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Instanciamos un sistema de actores
            var actorSystem = ActorSystem.Create("SmartPhoneActorSystem");

            // Desplegamos en el sistema de actores un nuevo Actor y nos quedamos con su referencia
            var smartPhoneActor = actorSystem.ActorOf<SmartPhoneActor>("smartPhoneActorOne");

            // Enviamos al actor smartPhoneActorOne un mensaje
            smartPhoneActor.Tell("Hola! ¿Que tal estas?");

            Console.ReadLine();
        }
    }
}

De esta manera, simplemente usando la referencia estática ActorSystem que nos ofrece la librería de Akka.Net, junto con la llamada al método Create() estaríamos creando un sistema de actores, el cual deberíamos de almacenar dicha referencia pues será el punto de acceso global al propio sistema de actores del la aplicación.

Nota: Todos los textos que se pasan por parámetro tanto en el método Create(“SmartPhoneActorSystem”) como en el método ActorOf(“smartPhoneActorOne”), se usan como identificadores dentro del sistema de actores de Akka.Net.

Con una forma similar usando la referencia al sistema de actores y el método ActorOf<ActorType>(), hemos podido crear un actor del tipo que habíamos definido previamente SmartPhoneActor. Al que seguidamente le he enviado un mensaje de texto saludando mediante el uso del método Tell(object message) aplicado sobre la referencia del actor creado en el paso anterior. Así que si ejecutamos la aplicación obtendríamos un resultado como el siguiente:

Continuamos con otro pasito más creando un tipo de mensaje específico y haciendo que el actor lo identifique y actúe en consecuencia:

using System;

namespace FirstAkkaApp.Actors.Messages
{
    public class SmsMessage
    {
        public SmsMessage(string text)
        {
            Text = text ?? throw new ArgumentNullException(nameof(text));
        }

        public string Text { get; }
    }
}

Creamos la clase SmsMessage, la cual representa un mensaje de texto enviado hacía el SmartPhoneActor.

using Akka.Actor;
using FirstAkkaApp.Actors.Messages;
using System;

namespace FirstAkkaApp.Actors
{
    public class SmartPhoneActor : UntypedActor
    {
        protected override void OnReceive(object message)
        {
            if(message is SmsMessage smsMessage)
                Console.WriteLine($"SmartPhoneActor New SMS received: {smsMessage.Text}");
            else
                Console.WriteLine($"SmartPhoneActor Receive untyped message: {message}");
        }
    }
}

Ahora modificamos el actor para que sea capaz de identificar cuando se le está enviando un mensaje de tipo SMS para identificarlo y mostrar un mensaje acorde. Ahora modificamos el método principal para enviar este nuevo mensaje al actor de la siguiente forma:

using FirstAkkaApp.Actors;
using FirstAkkaApp.Actors.Messages;
using System;

namespace FirstAkkaApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Instanciamos un sistema de actores
            var actorSystem = ActorSystem.Create("SmartPhoneActorSystem");

            // Desplegamos en el sistema de actores un nuevo Actor y nos quedamos con su referencia
            var smartPhoneActor = actorSystem.ActorOf<SmartPhoneActor>("smartPhoneActorOne");

            // Enviamos al actor smartPhoneActorOne un mensaje
            smartPhoneActor.Tell(new SmsMessage("Hola! Te escribo un SMS desde mi SmartPhone nuevo!!"));

            Console.ReadLine();
        }
    }
}

Como se puede apreciar ahora en vez de mandar un mensaje sin tipo, enviamos el nuevo mensaje creado. Con lo que el resultado ahora será como el siguiente:

Así de simple sería la identificación de distintos mensajes enviados a los actores, pero como hemos visto la lógica de identificación en el actor sigue siendo muy manual a nivel de código, es por esto que Akka.Net dispone de otro tipo de actor más acorde a la tarea que queremos realizar, y este sería el ReceiveActor. Que entre otras cosas posee la capacidad de identificar distintos mensajes enviados al actor de una forma mucho más simple que la vista anteriormente:

using Akka.Actor;
using FirstAkkaApp.Actors.Messages;
using System;

namespace FirstAkkaApp.Actors
{
    public class SmartPhoneActor : ReceiveActor
    {
        public SmartPhoneActor()
        {
            Receive<SmsMessage>( message => Console.WriteLine($"SmartPhoneActor New SMS received: {message.Text}"));
            Receive<LostCallMessage>(message => Console.WriteLine($"Lost call received!!"));
        }
    }
}

public class LostCallMessage{}

Con esta forma tan sencilla hemos creado el nuevo mensaje LostCallMessage, que representa una llamada perdida en nuestro smartphone, y además hemos cambiado la identificación del mensaje SmsMessage, ambos a través del método Receive<TypeMessage>() propio de esta nueva clase como es ReceiveActor. Se puede apreciar la sencillez en ésta nueva forma de identificar los mensajes que nos proporciona la librería de Akka.Net, modifiquemos ahora el método principal para simular una llamada perdida seguida de un mensaje de texto:

using FirstAkkaApp.Actors;
using FirstAkkaApp.Actors.Messages;
using System;

namespace FirstAkkaApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Instanciamos un sistema de actores
            var actorSystem = ActorSystem.Create("SmartPhoneActorSystem");

            // Desplegamos en el sistema de actores un nuevo Actor y nos quedamos con su referencia
            var smartPhoneActor = actorSystem.ActorOf<SmartPhoneActor>("smartPhoneActorOne");

            smartPhoneActor.Tell(new LostCallMessage());
            smartPhoneActor.Tell(new SmsMessage("Hola! Te he llamado pero no respondías... espero que estés bien!"));

            Console.ReadLine();
        }
    }
}

Del código anterior podemos esperar un resultado como el siguiente:

Se puede apreciar como el actor a procesado los mensajes en el mismo orden en el que le han sido enviados. De esta forma tenemos una implementación mucho mas rica que con el uso del UnTypedActor. Demos otro pasito más, y recordemos que os he comentado la capacidad de los actores para poder almacenar estado, pues utilicemos dicha característica para persistir un contador de las llamadas perdidas que recibirá el SmartPhoneActor:

using Akka.Actor;
using FirstAkkaApp.Actors.Messages;
using System;

namespace FirstAkkaApp.Actors
{
    public class SmartPhoneActor : ReceiveActor
    {
        private int _lostCalls = 0;

        public SmartPhoneActor()
        {
            Receive<SmsMessage>( message => Console.WriteLine($"SmartPhoneActor New SMS received: {message.Text}"));
            Receive<LostCallMessage>(message => {
                _lostCalls++;
                Console.WriteLine($"Lost call received! You have missed {_lostCalls} calls");
            });
        }
    }
}

Así podemos utilizar la variable _lostCalls como un contador de las llamadas perdidas que haya podido recibir el actor, recordemos que este estado interno del actor es solamente accesible por él mismo, es decir, desde fuera del código del propio actor resulta imposible de manipular, a no ser que el mismo actor lo muestre a través de algún mensaje enviado. Así que si modificamos el método principal para que simplemente envíe tres mensajes LostCallMessage, antes de enviar el SmsMessage, tendríamos una respuesta como la siguiente:

Pasemos ahora a hacer que el actor responda a los mensajes que ha recibido y para ello nos serviremos de una propiedad que poseen todos los actores en Akka.Net, se trata de la propiedad Context, desde donde como su propio nombre indica se ubica el contexto en el que se encuentra el actor, y por lo tanto podríamos identificar desde el mismo al emisor del mensaje inicial, esto último es posible hacerlo desde la propiedad Sender dentro del contexto. Con lo que utilizaremos la misma para enviar mensajes en respuesta.

//SmartPhoneActor.cs ...
Receive<SmsMessage>( message =>
            {
                Console.WriteLine($"SmartPhoneActor New SMS received: {message.Text}");
                Context.Sender.Tell(new SmsMessage("Hola! Estoy bien, me has pillado comprando en el supermercado"));
            });
//...

Si ejecutamos ahora la aplicación sucederá lo siguiente:

Nos aparecerá un mensaje indicando que desde el sistema de actores no ha podido enviar el mensaje de respuesta, y que por lo tanto se considera el mensaje como una deadLetter, un mensaje sin destinatario que será descartado. ¿Que acaba de ocurrir? Lo que sucede es que el contexto en el que enviamos el mensaje es desde el propio programa principal, osea se, la clase Program.cs que toda aplicación debe de tener, con lo que al no ser el contexto de respuesta un actor con su propia cola para los mensajes recibidos Akka.Net no puede encolarlo como respuesta y descarta dicho mensaje.

Para solucionar el problema que tenemos entre manos nos vamos a servir del método Ask<TypeMessage>(). Como bien hemos visto antes el problema de responder a un mensaje con el método Tell() es que todas las acciones realizadas con el mismo son asíncronas, y disparadas como un fire and forget, con lo que el sistema de actores buscará al posible destinatario o destinatarios del mismo y encolará el mensaje en sus correspondientes buzones, quedando a la espera los mismos mensajes de ser procesados por sus actores. Por otra parte, el método Ask(), lo que hace es quedarse a la espera de una respuesta al mensaje enviado, con lo que así el sistema de actores si que tiene un punto de retorno para dichos mensajes aunque el proceso que lo envíe no sea un actor en sí, ya que el mensaje enviado no se encolará si no que será procesado al instante.

Nota: Abusar de un enfoque de comunicación utilizando el método Ask() puede resultar en una perdida de rendimiento, ya que nos estamos saltando la capacidad de encolado de mensajes y procesamiento en orden de Akka.Net, es por esto que se recomienda siempre que sea posible el uso de Tell().

using FirstAkkaApp.Actors;
using FirstAkkaApp.Actors.Messages;
using System;

namespace FirstAkkaApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Instanciamos un sistema de actores
            var actorSystem = ActorSystem.Create("SmartPhoneActorSystem");

            // Desplegamos en el sistema de actores un nuevo Actor y nos quedamos con su referencia
            var smartPhoneActor = actorSystem.ActorOf<SmartPhoneActor>("smartPhoneActorOne");

            smartPhoneActor.Tell(new LostCallMessage());
            smartPhoneActor.Tell(new LostCallMessage());
            smartPhoneActor.Tell(new LostCallMessage());
            var response = smartPhoneActor.Ask<SmsMessage>(new SmsMessage("Hola! Te he llamado pero no respondías... espero que estés bien!"));

            Console.WriteLine(response.Result.Text);

            Console.ReadLine();
        }
    }
}

Con la modificación anterior en el método principal conseguimos que el resultado sea como el esperado:

Conclusión

Aquí concluimos este primer artículo donde repasamos los aspectos básicos de un sistema de actores, junto con sus ventajas e inconvenientes, aplicando un ejemplo además donde vemos también lo más básico en el uso de este sistema mediante Akka.Net. Con el que seremos capaces de construir aplicaciones reactivas.

Aún nos queda mucho camino por recorrer en éste ámbito, y es que tan solo hemos vislumbrado la punta del iceberg en cuanto a modelos de actores se refiere. Así que si te has quedado con ganas de más como yo, no os perdáis el siguiente artículo donde si que os puedo decir que comenzaremos a tratar con Máquinas de estado usando actores, uso del Stash del actor para retrasar el procesamiento de mensajes, el uso de contenedores de DI para el despliegue de actores y un largo etcétera.

Hasta pronto developers!