Modelo de actores con Akka.Net – II

Hola a todo el mundo! 🙂 Me complace mucho traeros otro capitulo más de la serie de Akka.Net. A modo de breve resumen esta tecnología nos facilita la creación de aplicaciones distribuidas y con una alta concurrencia de una forma relativamente sencilla, pero para evitar repetirme os dejo el enlace al primer artículo donde desenmarañamos de forma mas exhaustiva las ventajas que nos ofrece éste sistema.

En ésta ocasión vamos a tratar principalmente con el manejo del estado en actores, en cómo modificar el comportamiento de un actor de forma dinámica y empezaremos a ver de forma más teórica el uso de máquinas de estado finitas con su implementación bajo Akka.Net. Además trataremos el Stashing, una forma muy elegante de persistir temporalmente los mensajes que nuestro actor no es capaz de tratar en un momento dado.

Antes de comenzar con el contenido recordaros que aunque voy mostrando todo el código conforme es desarrollado, siempre podéis ir al repositorio de código en GitHub donde están todos los ejemplos, y de ésta parte en concreto sería el proyecto FirstAkkaApp.sln.

Cambio de estado en actores

Si recordáis del artículo anterior comentaba que los actores son capaces de procesar acciones sobre mensajes recibidos, y así implementamos a modo de ejemplo el SmartPhoneActor.cs, un actor que únicamente recibía mensajes y pintaba su contenido por consola simulando la recepción de un SMS. Pues ahora vamos a dar un pasito más allá y vamos a trabajar con la capacidad que tienen los actores de cambiar su comportamiento a los mensajes recibidos, es decir, modificar el estado interno del actor.

Para ello vamos a imaginarnos que el SmartPhoneActor.cs además de recibir SMS, puede también responder a llamadas, con la salvedad de que mientras que un usuario se encuentra en una llamada no es capaz de procesar nuevos SMS (En la realidad se que esto no es así 😛 ). Se podría traducir más técnicamente como que en ciertos momentos el actor se encuentra en un estado en el que no es capaz de procesar nuevos mensajes. Para ello Akka.Net nos muestra la instrucción “Become“, que permite al actor cambiar el comportamiento de los manejadores de mensajes.

Por facilidad para todos como comentaba antes vamos a seguir con el ejemplo usado en el artículo anterior, así que vamos ha extender el código de la siguiente forma:

    public class SmartPhoneActorV2 : ReceiveActor
    {
        private int _phoneNumber = 0;
        private int _lostMessage = 0;

        public SmartPhoneActorV2()
        {
            Receiver();
        }

        private void Receiver()
        {
            Receive<SmsMessage>(message =>
            {
                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine($"SmartPhoneActor New SMS received: {message.Text}");

                _lostMessage = 0;
            });

            Receive<IncomingCall>(message => {
                _phoneNumber = message.PhoneNumber;

                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine($"SmartPhoneActor New Incoming Call received: {_phoneNumber}");

                Become(Busy);

            });
        }

        private void Busy()
        {
            Receive<IncomingCall>(message => {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine($"SmartPhoneActor Is busy with other call sorry....");
            });

            Receive<SmsMessage>(message =>
            {
                _lostMessage++;

                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine($"SmartPhoneActor queued messages: {_lostMessage}");
            });

            Receive<HangUp>(message =>
            {
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine($"SmartPhoneActor Ended Call with: {_phoneNumber}");
                _phoneNumber = 0;
                Become(Receiver);
            });
        }
    }
    public class SmsMessage
    {
        public SmsMessage(string text)
        {
            Text = text ?? throw new ArgumentNullException(nameof(text));
        }

        public string Text { get; }
    }

    public class IncomingCall
    {
        public IncomingCall(int phoneNumber)
        {
            PhoneNumber = phoneNumber;
        }

        public int PhoneNumber { get; set; }
    }

    public class HangUp
    {
    }

Se puede apreciar como partiendo de la implementación de SmartPhoneActor.cs, he generado la clase SmartPhoneActorV2.cs como una segunda versión de éste mismo actor. En la que podemos ver que consta de dos variables privadas _phoneNumber y _lostMessage, enteros que representan el número de teléfono desde el que me están “llamando” y el número de mensajes perdidos que ha recibido el actor respectivamente.

A partir de ahora es donde cambia la cosa un poquito, en éste caso en vez de implementar en el constructor los handlers de los mensajes que el actor puede tratar vemos que se llama a un método privado de la propia clase denominado private void Receiver(), en éste método es donde se implementan los handlers propios y en concreto vamos a tratar dos mensajes que serían el SmsMessage.cs; simulando la recepción de un SMS, y el IncomingCall.cs; emulando una llamada entrante. Y si nos fijamos en el handler de éste último mensaje vemos que al final aparece una llamada al método que antes adelantábamos Become, para cambiar de estado el actor entero. El truco de ésta llamada reside en la llamada al otro método privado private void Busy() que se le pasa por parámetro, el cual redefine completamente los handlers que éste actor tuviera definidos, es decir, hace borrón y cuenta nueva de todos los handlers previamente configurados.

Este cambio de estado a Busy lo que hace es modificar el comportamiento para emular que el Smartphone se encuentra en una llamada, por lo tanto no es posible atender a los mensajes recibidos de IncomingCall.cs y SmsMessage.cs, cambiando el mensaje resultante (Junto con el color de la consola para que se aprecie mejor 😉 ). Y únicamente cuando procese el mensaje HangUp.cs (se ha colgado la llamada en curso). el actor volverá al estado previo Receiver usando otra vez el método Become, pudiendo de nuevo procesar de forma correcta nuevos SMS´s o llamadas.

Para clarificar ahora mismo los posibles estados y transiciones que podría tener el actor SmartPhoneActorV2.cs lo ilustro con el siguiente diagrama de estados:

1.1

Al final el resultado es el de intentar emular el comportamiento que tendría un teléfono móvil, mientras que no se encuentre en una llamada el usuario es capaz de acceder a su buzón de mensajes e interactuar con ellos, y en cuanto una llamada es recibida y contestamos pasamos a un estado en el que es imposible hablar por teléfono al mismo tiempo que observamos nuestros mensajes o contestamos a una llamada nueva sin colgar la actual.

Finalmente preparamos la aplicación de consola para que instancie un actor de éste tipo y ejecute un ejemplo como el siguiente:

    class Program
    {
        static void Main(string[] args)
        {
            string input = null;
            do
            {
                Console.WriteLine("EJEMPLOS DE AKKA.NET!!");
                Console.WriteLine("1 - Ejemplo simple de SmartPhoneActor. (Simple recepción de mensajes)");
                Console.WriteLine("2 - Extender SmartPhoneActor con cambio de estado. (Cambia el comportamiento al recibir mensajes)");
                Console.WriteLine("'exit' - para salir");
                Console.WriteLine("Introduce el número de ejemplo a ejectuar o 'exit' para salir....");

                input = Console.ReadLine();

                switch (input)
                {
                    case "1":
                        FirstAkkaExample();
                        break;
                    case "2":
                        StateChangeExample();
                        break;
                }

                Console.WriteLine();
            } while (input != "exit");

           

        }

        private static void FirstAkkaExample()
        {
            // 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<SmartPhoneActorV1>("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);
        }

        private static void StateChangeExample()
        {
            // 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<SmartPhoneActorV2>("smartPhoneActorTwo");

            smartPhoneActor.Tell(new SmsMessage("Ey! Cuanto tiempo! ahora en un rato te llamare"));
            smartPhoneActor.Tell(new IncomingCall(664534231));
            smartPhoneActor.Tell(new SmsMessage("Su pedido de Amazon ha sido entregado!"));
            smartPhoneActor.Tell(new IncomingCall(667584934));
            smartPhoneActor.Tell(new SmsMessage("El mensajero no pudo realizar la entrega"));
            smartPhoneActor.Tell(new HangUp());
            smartPhoneActor.Tell(new SmsMessage("Me ha gustado mucho hablar contigo, seguiremos en contacto!"));

            Console.ReadLine();
        }
    }

Antes de nada comentar que he modificado un poco el arranque para que permita parámetros por entrada en la consola y así poder ejecutar de forma sencilla tanto el ejemplo de código visto en el artículo anterior de Akka.Net como éste nuevo ejemplo. Desde el nuevo que se encuentra en el método private static void StateChangeExample(), voy a:

  1. Instanciar un nuevo sistema de actores.
  2. Crear un nuevo SmartPhoneActorV2 sobre dicho sistema.
  3. Enviar un flujo de mensajes en el siguiente orden:
    • SmsMessage, IncominCall, SmsMessage, IncomingCall, SmsMessage, HangUp, SmsMessage

Si ejecutamos dicho ejemplo obtendremos un resultado como el siguiente:

Se aprecia claramente como el flujo de mensajes es correcto (verde) hasta que recibe una llamada, y el actor cambia de estado a Busy, donde cualquier nuevo mensaje responde diciendo que no es capaz de procesarlo (rojo), terminando con la recepción de una finalización de la llamada (amarillo) y posteriormente se comprueba que si es capaz de procesar de nuevo SMS´s.

Y con estas sencillas líneas de código hemos podido crear un actor con cambio de comportamiento dinámico aprovechando para ello la capacidad del método Become que nos otorga Akka.Net.

Maquinas de estado finito

Gracias al uso de máquinas de estado finito es muy fácil para nosotros los programadores el modelar comportamientos que cambien en base a ciertos eventos y todo visto desde una forma de diagrama. El concepto de evento es usado como hilo de conexión entre la transición de un estado del sistema a otro. Y el concepto de estado se identifica como el conjunto de acciones que el sistema es capaz de hacer dependiendo del evento capturado. Dicho de la siguiente forma, cuando un estado recibe un evento éste es capaz de reaccionar de las siguientes formas:

  1. Ignora el evento
  2. mantiene el mismo estado.
  3. cambia de estado.

Esta figura que se está mostrando os debería de sonar, ya que sería algo parecido a lo que hemos implementado en la sección previa (Figura 1.1) donde trabajamos con los cambios de estado en actores. Pues bien estos diagramas de transición de estados normalmente se dibujan como un grafo dirigido, donde cada vértice representa un estado y cada arista sería un evento.

Si te estas preguntando porqué estoy volviendo ahora a un concepto más teórico es debido a que el concepto de máquinas de estado finito es lo suficientemente importante a la hora de construir sistemas concurrentemente complejos. Con estas capacidades que nos otorga Akka.Net es posible crear un actor que reacciona activamente a los mensajes que les son enviados nada más instanciarse el actor.

Pero ahora os planteo otro problema, si volvemos otra vez al ejemplo implementado de SmartPhoneActorV2.cs, cuando éste mismo se encontraba en estado Busy tanto los mensajes de SMSMessage y IncomingCall son recibidos en un estado en el que el actor no es capaz de tratarlos en ese momento. ¿Entonces que pasa con estos mensajes? ¿No somos capaces de tratarlos? ¿Es posible almacenarlos de alguna manera? La respuesta a estas preguntas obviamente es afirmativa. Ya que somos nosotros mismos los que estamos implementando el actor, nada nos impide crear algún tipo de colección de mensajes e ir guardando uno a uno…. un momento un momento no vayas tan rápido!

Esta frenada en seco tiene una explicación, como toda buena librería que se precie Akka.Net ya dispone de herramientas propias para tratar de forma elegante y precisa este tipo de situaciones, y esto mismo es lo que vamos a ver en la siguiente sección.

Stashing

Pues bien, este concepto nace para solucionar el problema comentado anteriormente, y se trata nada más y nada menos de almacenar todos los mensajes que el actor recibe y que no es capaz de procesar en dicho momento. Hay que pensar en este Stash como en una cola donde Akka almacena en orden de llegada los mensajes, y esto es muy importante ya que una de las cosas que siempre asegura Akka.Net es el envío de mensajes en orden. Es por esto mismo que hemo echado el freno al final de la sección anterior, ya que, no pongo para nada en duda que cualquiera de vosotros podría implementar manualmente algún tipo de cola que también fuese capaz de procesar y almacenar los elementos en orden pero ¿Para que reinventar la rueda?

Antes de comenzar con algún ejemplo de código (Que se que lo estáis deseando 😛 ), vamos a ver alguna característica adicional que nos ofrece el Stash a la hora de recuperar los mensajes, y se trata de los siguientes métodos:

  • Unstash: Extrae solo el siguiente mensaje del Stash.
  • UnstashAll: Extrae todos los mensajes manteniendo su orden de llegada.
  • UnstashAll(Predicate): Extrae todos los mensajes del Stash que cumplan el predicado, y por supuesto manteniendo el orden de llegada entre los mismos.

y ahora si que si, vamos con el ejemplo de código:

    public class SmartPhoneActorV2 : ReceiveActor, IWithUnboundedStash
    {
        private int _phoneNumber = 0;
        private int _lostMessage = 0;

        public IStash Stash { get; set; }

        public SmartPhoneActorV2()
        {
            Receiver();
        }

        private void Receiver()
        {
            Receive<SmsMessage>(message =>
            {
                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine($"SmartPhoneActor New SMS received: {message.Text}");

                if (_lostMessage > 0) _lostMessage--;
            });

            Receive<IncomingCall>(message => {
                _phoneNumber = message.PhoneNumber;

                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine($"SmartPhoneActor New Incoming Call received: {_phoneNumber}");

                Become(Busy);

            });
        }

        private void Busy()
        {
            Receive<IncomingCall>(message => {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine($"SmartPhoneActor Is busy with other call sorry....");
                Stash.Stash();
            });

            Receive<SmsMessage>(message =>
            {
                _lostMessage++;

                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine($"SmartPhoneActor queued messages: {_lostMessage}");
                Stash.Stash();
            });

            Receive<HangUp>(message =>
            {
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine($"SmartPhoneActor Ended Call with: {_phoneNumber}");
                _phoneNumber = 0;
                Become(Receiver);
                Stash.UnstashAll();
            });
        }
    }

Únicamente implementando la interfaz IWithUnboundedStash en el actor, ésta nos obliga a implementar la propiedad Stash de tipo IStash, la cual representa (Como su propio nombre indica) la pila de mensajes donde serán almacenados. Y ahora usando los métodos de la propiedad como Stash() y UnstashAll(), seremos capaces de almacenar el mensaje actual y obtener todos los mensajes almacenados. Con todo esto la implementación emula que cuando el SmartPhoneActorV2.cs pasa a estado Busy (contestando a una llamada), para evitar perder los mensaje pendientes que recibamos en éste estado vamos a aplicar el método Stash(), en los handers de los mensajes SMSMessage y IncomingCall. Y por el contrario cuando en éste mismo estado se reciba el mensaje HangUp; emulando la terminación de la llamada, aplicaremos el método UnstashAll() posteriormente de cambiar de estado a Receiver recuperando todos los mensajes pendientes.

Si ejecutamos ahora el ejemplo con el código anterior obtendremos lo siguiente:

Es posible que estés viendo algo extraño y que pueda inducir confusión. Por eso vamos a repasar lo que ha ocurrido mensaje a mensaje:

  1. Receiver:SmSMessage -> Puede recibir el mensaje sin problemas y muestra el texto
  2. Receiver:IncomingCall -> Recibe el la llamada y es contestada. Pasa a estado Busy.
  3. Busy:SmSMessage -> No puede procesar el mensaje. Encola en Stash y muestra el conteo pendiente.
  4. Busy:IncomingCall -> No puede procesar el mensaje. Encola en Stash y muestra el error.
  5. Busy:SmSMessage -> No puede procesar el mensaje. Encola en Stash y muestra el conteo pendiente.
  6. Busy:HangUp -> Procesa el mensaje. Pasa a estado Receiver y desencola los mensajes del Stash.
  7. Receiver:SmSMessage -> Procesa el primer mensaje del Stash.
  8. Receiver:IncomingCall -> Procesa el segundo mensaje del Stash. Pasa a estado Busy.
  9. Busy:SmSMessage -> Procesa el tercer mensaje del Stash pero no puede porque vuelve a estar Busy. Lo encola de nuevo en el Stash.
  10. Busy:SmSMessage -> No puede procesar el ultimo mensaje. Encola en Stash y muestra el conteo pendiente.

¿Te has dado cuenta del fallo en el conteo de mensajes pendientes…. 😉 ?

Todo ha funcionado tal y como debía de suceder, lo único que podría llevar a liarnos era el caso de que al desencolar los mensajes se vuelve a obtener en el segundo mensaje un IncomingCall, haciendo que se vuelva a responder a la llamada y por lo tanto pasa otra vez a estado Busy. Debido a esto es por lo que tanto el último mensaje que había en el Stash como el último que llega a recibir de forma normal no son procesados de forma correcta. Con todo esto hay un aspecto muy importante a tener en cuenta a la hora de trabajar con esta feature, y es que en cuanto utilices la llamada al método UnStashAll(), todos los mensajes que hubieran en éste pasan, por así decirlo, con mayor prioridad en la cola del actor, por lo que serán procesados estos antes que cualquier otro mensaje que el actor recibiese por otro lado.

Siguiendo con el ejemplo, resulta extraño en la implementación que simula un smartphone, que éste sea capaz de almacenar llamadas perdidas y volver a recuperar las llamadas con los usuarios de forma inmediata. Lo normal sería que el usuario pudiese recuperar o ver los mensajes pendientes pero las llamadas se quedan como llamadas perdidas y no vuelven a bloquear el teléfono inmediatamente. Para desarrollar esto en nuestro actor vamos a servirnos del método UnstashAll(Predicate) para desencolar únicamente los mensajes de tipo SmSMessage, evitando así el bloqueo de nuevo cuando se recuperan los mensajes que no han podido procesarse. Veámoslo en código:

            Receive<HangUp>(message =>
            {
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine($"SmartPhoneActor Ended Call with: {_phoneNumber}");
                _phoneNumber = 0;
                Become(Receiver);
                Stash.UnstashAll(e => e.Message is SmsMessage);  // Check if is a SmsMessage
            });

Tan sencillo como esa simple línea de evaluación de un tipo de mensaje en concreto dentro del handler del mensaje HangUp; el cual se encarga de liberar los mensajes encolados y cambiar de estado de nuevo, y ya tendríamos nuestra nueva feature implementada para evitar el bloqueo. Así que si ejecutamos de nuevo el ejemplo obtendremos un resultado como el siguiente:

Y como vemos el resultado es el esperado, ahora únicamente se ha recuperado los mensajes del Stash y siendo estos del tipo SmsMessage.

Nota: Ten en cuenta un aspecto importante de esto último que hemos implementado. Al filtrar que únicamente queremos recuperar los mensajes de un tipo específico y los otros no, nos hemos olvidado de que los otros mensajes que se están encolando en el Stash, los IncomingCall. Nunca son liberados de éste por lo que a la larga estaríamos generando un memory leak. En ejemplos de este tipo no pasa nada pero si esto ocurre en una aplicación con alta concurrencia el problema podría ser grave…

Conclusión

Con esto concluimos aquí este nuevo artículo sobre Akka.Net. Una de las cosas que más me gustan de Akka, o mejor dicho del modelo de actores, es la simplicidad con la que es posible gestionar tanto cambios de estado con grafos dirigidos como hemos visto en éste artículo, y además lo fácil y eficiente que gestiona la concurrencia a unos niveles más altos que los tratamientos habituales. Personalmente es una herramienta que yo la utilizo mucho en mi trabajo actualmente y debido a ello es cómo me fui haciendo callo con ésta tecnología. Es una lástima que no sea tan conocida o extendida como se merece porque de verdad os aseguro que una vez que os acostumbréis podréis desarrollar aplicaciones reactivas y distribuidas de una forma totalmente sencilla.

No nos confundáis con todo este discurso de conclusión que para nada hemos visto el potencial aún de ésta tecnología, aún nos queda mucho camino por ver y muchos ejemplos de código con los que tratar. Y que por supuesto yo estaré aquí al pie del cañón para enseñaros y ayudaros en todo lo que pueda acerca de Akka.Net. Próximamente si que os puedo aventurar que empezaremos a ver opciones de configuración específicas para trabajar con Akka, como el uso de Inyección de dependencias y logging, lo que significa que ya empezaremos a ver un uso más específico al que se debería de aplicar en aplicaciones reales, y nos alejaremos un poco de estos ejemplos de juguete 😉 .

Espero que os haya gustado el artículo tanto como a mi programarlo, nos vemos en el siguiente!!