Archivo de la etiqueta: futuros

Futuros y promesas,… y también monadas. Implementando el patrón

“Los modelos NIO no bloqueantes y la programación asíncrona y reactiva son paradigmas que poco a poco van adoptándose y utilizando más.”

Los modelos NIO no bloqueantes y la programación asíncrona y reactiva son paradigmas que poco a poco van adoptándose y utilizando más. No creo que diga una tontería al afirmar que muy probablemente serán la norma en un futuro cercano. La razón es clara: aunque no se obtiene un rendimiento mejor que los modelos tradicionales sincrónicos, si son modelos mucho más eficientes. Obtienen el mismo rendimiento con menos recursos al tener un mejor control de los hilos, uno de los recursos más preciados en un sistema operativo.

Futuros y promesas

Futuros y promesas

Para poder articular una programación asíncrona y reactiva es necesario utilizar patrones adaptados a este tipo de programación. Puedes utilizar el patrón observable o las señales,  pero casi podríamos decir que tu camino te llevará irremisiblemente al patrón futuro en cuando tienes que integrar varios resultados asíncronos. Si además nuestra implementación del patrón futuros-promesas cumple con las propiedades de las monadas se nos abre un mudo de posibilidades. Con la programación funcional, la composición de futuros se podrá hacer de una manera elegante obteniendo un código más legible.

Esto permite realizar DSL’s (Domain Specific Language) y separar la lógica de negocio de los efectos de lado. Por ahora “… ahí lo dejo”; prefiero no perder foco en el objetivo de este artículo, pero en mi mente esta tratar todo esto en una nueva reseña. En este artículo entraré a desarrollar todos estos nuevos conceptos intentando desmitificarlos, porque al principio todo lo de “Futuros”, “Promesas” y “Mónadas” puede que suene “muy místico”. Lo haré apoyado en mi propia implementación del patrón Futuros y promesas que está disponible en mi GitHub.

[Logo GitHub] AlternativeFuture

La implementación está basada “fuertemente” en el Api de Scala de futuros y sólo recordar que Akka en su versión de Java la exporta directamente y que además los futuros de Scala son también monadas. Cuando digo que me apoyo en el Api de Scala me refiero a seguir la definición de su contrato, de hecho, no me interesaba el código fuente y si hacer el ejercicio de, a partir de su interfaz, realizar mi propia implementación en Java. El código que podéis ver y utilizar es todo mio. Hecha esta aclaración es el momento de empezar a entrar en faena.

Intentaré hacer un acercamiento de dentro a fuera. Primero pondré el foco en las ideas principales para ir después abriendo el zoom y conseguir tener la imagen completa.

Futuros

¿Qué es un futuro?” Pongámonos en contexto. Imaginemos dos procesos: uno será el principal que llamará a un método que devuelve un futuro de un valor. El otro será el que calcule realmente el valor de manera asíncrona ejecutado en un segundo plano con respecto al principal. Voy a poner un ejemplo para hacerlo más didáctico. Ana (el hilo A, el principal) le dice a Bartolo (el hilo B) que haga la colada (llamada al método)

Llamada asíncrona

1. Llamada asíncrona

El proceso principal obtiene un futuro como resultado de llamar a un método que se lanzará en segundo plano gracias al hilo B. En el ejemplo Bartolo manda una nota (representa el futuro) a Ana que le permitirá apuntar lo  que se quiere que se haga con la colada cuando haya terminado Bartolo. El hilo A no queda a la espera del resultado de la acción ejecutada por el hilo B y queda liberado para realizar otras operaciones. En el ejemplo Ana no espera a que Bartolo termine y  puede seguir haciendo otras tareas.

2. Se devuelve un futuro

2. Se devuelve un futuro

Este futuro es en realidad un artefacto que permite asignar, desde el proceso principal una función callback. En el siguiente paso, Ana decidirá que es lo que quiere hacer cuando termine la colada, es decir, asignará la función callback que se ejecutará cuando se obtenga el resultado del segundo proceso ejecutado por Bartolo.

Asignar callback

3. Asignar callback

Cuando, gracias al segundo proceso, se haya calculado finalmente el valor, se ejecutará la función de regreso al que se le pasará como argumento el valor final obtenido. En el ejemplo tenemos la función “planchar la ropa” y el resultado del proceso: “la ropa lavada”.

Resultado del futuro

4. Resultado del futuro

Contextos de ejecución

La ejecución de las funciones callback con el valor final como argumento se hará dentro de lo que se llama un contexto de ejecución. Este no es más que un pool de hilos que permite independizar la ejecución de la función con el valor final tanto del hilo principal como del segundo proceso, es decir, se ejecutarán en un tercer hilo “reutilizable”. En el ejemplo que estamos siguiendo es Carlos (el hilo C) el que plancha la  ropa.

Ejecución de la función callback con el valor del futuro

Ejecución de la función callback con el valor del futuro

En la implementación del patrón que he propuesto a cada una de las funciones callback y de orden superior definidas en el interfaz futuro se le puede pasar un contexto de ejecución.

Más cosas de los futuros

A grandes rasgos, esta es la esencia: el futuro permite configurar desde la ejecución principal que hacer cuando finalmente exista el valor. Y se hace sin tener que bloquear hilos porque sino arruinamos la asincronía.

Teniendo esto en mente y sin perder de vista lo simple de este concepto, base de todo, voy a ir añadiendo nuevos conceptos e ideas que nos permitirá ir completando la visión general del patrón.

Dos caminos

Añadiendo una nueva capa a las funciones callback. El resultado de la ejecución de cualquier método puede ser, o bien su valor, o bien que su ejecución se haya saldado con un error, luego el resultado final que contiene un futuro puede ser:

  • un valor correcto
  • o también una excepción.

¿Por qué no contemplar esta circunstancia con dos caminos? Un camino para cuando el valor sea correcto con su correspondiente función callback (el método onSuccess) y otro que nos permita definir otra función de regreso cuando se produzca un error (el método onFailure)


Signatura de un onSuccess y onFailure de AlternativeFuture[T]:

onSuccess

void onSuccesful( final Consumer<T> function, final Executor executor );

El argumento function es una función cuyo argumento es de tipo T (es el tipo del valor final del futuro) y resultado void.

function: T => void

Esta será la función que se ejecutaría cuando de resultado final del futuro ha ido bien.

onFailure

void onFailure ( final Consumer<Throwable> function, final Executor executor );

En este caso el argumento function es una función cuyo argumento es un Throwable (la excepción que se reportaría si se produciría un error) y resultado void.

function: Throwable => void

Está será la función que se ejecutará cuando el futuro se haya solventado con un error.


¿Cuando se ejecuta la función callback?

Para dar respuesta a esta pregunta nos debemos fijar en que existan las dos cosas necesarias para la ejecución de la función: que exista la función y que exista el valor. Hay entonces dos posibilidades:

  • Puede que ya haya asignada una o varias funciones callback al futuro, entonces la ejecución de las mismas será cuando se obtenga el valor.
  • O bien puede ser que se obtenga primero el valor y el disparador de que la función se ejecute será la propia asignación de la función callback.

Es importante tener en mente que un futuro (“barra” promesa, esto lo explicaré más adelante), sólo se le puede asignar el valor una vez, además otra característica es que una función callback sólo se ejecutará una vez. Sin embargo se puede asignar al futuro, a lo largo del tiempo, todas las funciones callback que se deseen.

Promesas

Si has llegado hasta aquí probablemente ya te habrá asaltado la duda: ¿Cómo se da valor al futuro? ¿Cómo se le asigna un valor? La respuesta: a través de la promesa.

Podemos decir que la promesa y el futuro son las dos caras de la misma moneda y que una promesa está relacionada con un futuro. La idea básica que debemos tener en mente es la siguiente:

  • el futuro permite asignar la función callback para tomar decisiones una vez se haya obtenido el valor
  • la promesa permite dar valor a ese futuro.

Si volvemos al ejemplo anterior, será más sencillo de entender. Si recordamos el proceso principal llamaba a un método que permite en un segundo proceso calcular un valor de manera asíncrona en un tiempo indeterminado (puede ser antes o después). Mientras que el proceso principal tiene como herramienta al interfaz futuro que le permite indicar que es lo que se debe de hacer una vez se haya calculado el valor, el interfaz de la promesa será el instrumento que poseé  el segundo proceso encargado de calcular el valor para asignar el valor a ese futuro. En síntesis Ana usaría el futuro y Bartolo la promesa.

Diagrama de flujo Promesa-Futuro

Diagrama de flujo Promesa-Futuro

Diagrama de secuencia Promesa-Futuro

Diagrama de secuencia Promesa-Futuro


Implementación

Si echáis un vistazo al código, podéis observar que sólo existe una implementación que implementa las dos interfaces: la promesa y el futuro.

La clase AlternativePromiseImp tiene un atributo que permite almacenar el valor del futuro. Este se asignará gracias a los métodos que expone el interfaz promesa AlternativePromise.

Existen dos atributos más en AlternativePromiseImp que son del tipo de cola FIFO (el primero que entra será el primero que sale). Permiten almacenar las funciones callback gracias a los métodos expuestos por el interfaz AlternativeFuture.

Una de las colas guardará las funciones que se van a ejecutar cuando mediante la promesa relacionada se asigne un valor correcto al futuro. Las funciones se van añadiendo a esta cola utilizando el método ‘onSuccesful‘ del interfaz AlternativeFuture.

La otra cola FIFO almacenará las funciones callback que se ejecutarán cuando la promesa relacionada se resuelva con un error. De la misma manera, se van añadiendo estas las funciones gracias al método ‘onFailure‘ del interfaz AlternativeFuture.

Bien cuando se asigna un valor a la promesa o bien cuando se añade una función callback al futuro se comprueba que existan los requisitos necesarios (la función y el valor del futuro). Si es así se ejecutará las funciones callback incluidas en la cola de callbacks con el valor final. Según se vayan ejecutando las funciones se eliminarán de la cola.


El Futuro como mónada. Programación funcional con AlternativeFuture

Se puede dar una vuelta de tuerca más y seguir añadiendo capas a la cebolla. El patrón Futuro/promesa permite adoptar un nuevo patrón funcional: la monada. Este artículo ya es lo suficientemente extenso, quizás con ya demasiados conceptos así que dejo pospuesto introducir la composición mónadica con AlternativeFuture y os emplazo a un siguiente post continuación de este.

M.E.

Akka por ejemplos. Sobre supervisión, bus de eventos, …

Esta es la segunda entrega de Akka por ejemplos. La primera trató sobre cual es el funcionamiento de la cola de mensajes de un actor, que es un router y como se envían mensajes entre actores.

Ahora se tratarán temas como el control de fallos y la supervisión de actores, el bus de eventos de Akka y sobre los ‘Dead Letters’

Entrada anterior: Akka por ejemplos

Sin más vamos con los ejemplos.

Todo el código fuente relacionado con esta serie de posts está disponible en mi repositorio de GitHub

[Logo GitHub] AkkaActorsAndFutures


Control de fallos: Supervisión y monitorización.

[Supervisión padre-hijo en Akka]

[Supervisión padre-hijo en Akka]

En Akka la robustez a fallos se basa en la supervisión de actores con jerarquía padre-hijo. El actor que delega tareas en los actores que crea es el que ‘supervisa‘ y decide que hacer cuando se produce un fallo en alguno de sus hijos.

Se crea así una jerarquía en forma de árbol en la que la que los actores padres supervisan a los actores hijos. El actor ‘user‘ de Akka será el padre de primera generación de actores que hayamos creado.

Esto es realmente lo que te da la robustez (‘resilence‘)  que un sistema reactivo necesita. Esta robustez se pierde cuando se intenta en vez de tener esta jerarquía, ir a un esquema con una mentalidad más tradicional de inyección de dependencias y se cae en la tentación de inyectar actores a otros actores.

Cuando un actor falla y se produce una excepción, su supervisor puede hacer, o bien reiniciar el actor, parar el actor, escalar la excepción o absorber la excepción y continuar con la ejecución.

El sistema de supervisión de Akka permite tener la posibilidad de diferente respuesta del supervisor dependiendo del tipo de excepción y si esta afecta a todos los actores supervisados o sólo al que ha fallado. Esto junto con lo anterior permite implementar fácilmente patrones,  por ejemplo de tipo ‘circuit breaker‘, si uno de los actores falla.

Akka tiene la posibilidad de establecer eventos o disparadores en el ciclo de vida de un actor. Por ejemplo antes de crearlo, antes y después de reiniciarlo y cuando se para.

Ademas del concepto de supervisión, en Akka existe el de monitorización. Cualquier actor, puede ‘monitorizar‘ a cualquier otro, lo haya creado o no. El primero obtiene un mensaje ‘Terminated‘ cuando el monitorizado ha terminado. Esto permite modificar su comportamiento o cambiar su estado. En los ejemplos siguientes el actor supervisor también hace de ‘monitor‘ de sus hijos.

Para poder mostrar con ejemplos como es el comportamiento de las diferentes estrategias a seguir, he creado en el proyecto dos actores: ActorNoTypedDummyCheckLifeCycle y ActorNoTypedLetItCrash.

ActorNoTypedDummyCheckLifeCycle se utiliza para comprobar los eventos que se disparan y por los ‘estados‘ que pasa un actor cuando es supervisado con diferentes estrategias. Concatena en un atributo ‘state‘ los diferentes pasos por los ‘hooks‘ del ciclo de vida de un actor:

  • Método ‘aroundPreStart‘: Primer evento que se dispara cuando se inicia un actor. Sólo se ejecuta cuando se inicia un actor por primera vez.
  • Método ‘preStart’: Se lanzan cuando se inicia un actor o después de que se haya reiniciado.
  • Método ‘aroundPreRestart’: Cuando un actor va a ser reiniciado, se dispara antes de parada el actor.
  • Método ‘preRestart‘: También se ejecuta antes de parar el actor cuando va a ser reiniciado. La llamada es después de la del método ‘aroundPreRestart
  • Método ‘postStop‘: Evento que se llama, o bien cuando se para el actor, o bien antes de reiniciarlo (en este último caso justo después de que se llame al método ‘preRestart‘)
  • Método ‘postRestart‘: Se lanza después de reiniciar el actor.
  • Método ‘aroundPostStop‘: Se realiza la llamada justo antes de parar definitivamente el actor.

El actor ActorNoTypedLetItCrash es el que creará y hará de supervisor de ActorNoTypedDummyCheckLifeCycle. En su constructor,  permite definir por parámetro la estrategia de supervisión. Su comportamiento será en de hacer de proxy en el envío de mensajes a  ActorNoTypedDummyCheckLifeCycle hasta que este el actor hijo se pare o cuando el mismo se haya reiniciado.

Para mostrar el comportamiento de las diferentes estrategias y cual es el ciclo de vida especifico de los actores (cuando se llama a los diferentes ‘hooks‘) he codificado en el proyecto los siguientes ejemplos.

Estrategia de supervisión por defecto

Test: ActorLetItCrashTestDefault.java

Comando maven
mvn -Dtest=com.logicaalternativa.examples.akka.supervisorstrategy.ActorLetItCrashTestDefault test

Si se produce un excepción en un actor hijo, la estrategia por defecto en Akka es la de reiniciar el actor hijo, excepto si es un error irrecuperable:

  •  cuando se ha producido una excepción al inicializar el actor (ActorInitializationException)
  • cuando ya se había parado el actor(ActorKilledException)

Esta estrategia cobra sentido cuando se quiere mantener un sistema estable. La idea subyacente es que cuando se produce un error el sistema debería volver a un estado consistente y esto se consigue reiniciando el estado del actor.

En este ejemplo, los pasos son:

  1. Se envía un primer mensaje ‘state‘ al actor ActorNoTypedLetItCrash. Este hará el reenvío del mensaje al actor hijo (ActorNoTypedDummyCheckLifeCycle) que devolverá la concatenación de los diferentes métodos por los que ha pasado hasta ese momento.
  2. Después se envía un mensaje con una excepción para que el actor hijo provoque la excepción.
  3. Por último se vuelve a enviar un mensaje ‘state‘ para ver por los eventos que se ejecutan cuando se reinicia el actor.
  4. Finalmente para poder ver todo el flujo, se para el actor.

El ejemplo permite comprobar el flujo completo del paso de los diferentes eventos a lo largo de la vida del actor hijo. En este caso con la estrategia de supervisión por defecto, es este:

Se llama al constructor
 => Llamada a aroundPreStart
  => Llamada a preStart
   => [[exception]] Se provoca una excepción
    => Llamada a aroundPreRestart
     => Llamada a preRestart
      => Llamada a postStop
       => Llamada al constructor (Se vuelve a crear el actor)
        => Llamada a aroundPostRestart
         => Llamada a postRestart
          => Llamada a preStart
           => Llamada a aroundPostStop
            => Llamada a postStop

Estrategia de supervisión: Escalar la excepción

Test: ActorLetItCrashTestEscalate.java

Commando maven
mvn -Dtest=com.logicaalternativa.examples.akka.supervisorstrategy.ActorLetItCrashTestEscalate test


¿Qué ocurre cuando se elige una estrategia de supervisión de tipo ‘escalate‘? En este caso la excepción en el actor hijo provoca el error también en el actor supervisor. El comportamiento final depende entonces de la propia estrategia de supervisión de este último.

El ejemplo puede ayudar a verlo más claro. En este caso se configura el supervisor (ActorNoTypedLetItCrash) para que la estrategia de supervisión sea la de escalar la excepción. Cuando se provoca una excepción en el actor supervisado, el resultado es el mismo que si se hubiera producido en el supervisor.

El este caso el supervisor tiene la estrategia de supervisión por defecto, que lo reinicia, volviéndose a crear el actor hijo.

En el test se comprueba este hecho cuando se valida todo el ciclo de vida del actor hijo. Se arranca y después de la excepción, se para, para después volver a arrancar sin reinicio. Esto último provocado porque al reiniciarse actor supervisor, el hijo se para y se vuelve a crear:

Llamada a constructor
 => Llamada a aroundPreStart
  => Llamada a  preStart
   => [[exception]] Se provoca una excepción
    => Llamada a aroundPostStop
     => Llamada a postStop
      => Llamada al constructor (Se vuelve a crear el actor)
       => Llamada a aroundPreStart
        => Llamada a preStart
        => Llamada a aroundPostStop
         => Llamada a postStop

Estrategia de supervisión ‘resume’: ‘Absorber’ la excepción.

Test: ActorLetItCrashTestResume.java

Comando maven
mvn -Dtest=com.logicaalternativa.examples.akka.supervisorstrategy.ActorLetItCrashTestResume test

Si el supervisor adopta una estrategia de ‘resume‘ si se produce un error la excepción no produce ni el reinicio, ni la parada del actor supervisado. Aunque se haya producido el error, sigue levantado a la espera de procesar nuevos mensajes.

El test ayuda a comprender este tipo de supervisión. Se comprueba que cuando se produce una excepción al procesar un mensaje, no se hace ninguna llamada a los métodos del ciclo de vida, ni tampoco se escala la excepción. El siguiente mensaje lo procesa correctamente.

El ciclo de vida del actor supervisado con este tipo de supervisión será:

Llamada a constructor
 => Llamada a aroundPreStart
  => Llamada a preStart
   => [[exception]] Se provoca una excepción
    => Llamada a aroundPostStop
     => Llamada a postStop

Estrategia de supervisión ‘stop’: Parar el actor supervisado.

Test: ActorLetItCrashTestStop.java

Comando maven
mvn -Dtest=com.logicaalternativa.examples.akka.supervisorstrategy.ActorLetItCrashTestStop test

En este caso, se para el actor supervisado cuando al procesar un mensaje ocurre una excepción.

En el ejemplo se fuerza esta situación. Si se provoca una excepción en el actor supervisado se comprueba que el actor no se vuelve a reiniciar. De hecho si se le vuelve a enviar un mensaje de estado, este se envía a la cola de ‘Dead Letters‘ porque la referencia al actor ya no apunta a un actor válido.

El ciclo de vida del actor es el siguiente:

Llamada a constructor
 => Llamada a aroundPreStart
  => Llamada a  preStart
   => [[exception]] Se provoca una excepción
    => Llamada a aroundPostStop
     => Llamada a postStop

Los mismos pasos que con la estrategia ‘resume‘. Pero en este caso no ha sido necesario parar el actor en el test porque ‘muere‘ después de provocarse la excepción.

Bus de eventos interno de Akka

Test: PublishSimpleSuscribeTest.java

Comando maven
mvn -Dtest=com.logicaalternativa.examples.akka.bus.PublishSimpleSubcribeTest test

Akka poseé un bus interno que permite el envío de mensajes siguiendo el patrón publicador-suscriptor. Un ‘publicador‘ envía un mensaje sin saber quien van a ser los receptores. Los actores que quieran recibir ese tipo de mensajes deberán suscribirse a esa publicación.

[Bus de eventos de Akka]El paradigma cambia. En vez de enviar un mensaje a un actor concreto, es decir, “(Por favor) haz esto”, cuando se publica un mensaje en el bus se está diciendo “(A quien pueda interesar) ha ocurrido esto

En este caso el ejemplo muestra un uso sencillo del bus de Akka para mostrar este patrón.

Se levantan dos actores suscriptores (del tipo ActorNoTypedLogEvent) que se suscribirán a mensajes de tipo Integer. La implementación de este actor es sencilla: registra los mensajes del tipo a los que está suscrito y si se le envía un mensaje ‘lastMessage‘ devolverá el último mensaje que ha registrado.

En el test después de levantar a los dos suscriptores se envía un mensaje al bus de Akka. Después se comprueba que el mensaje le ha llegado a ambos.

Dead letters

Test: DeadLettersTest.java

Comando maven
mvn -Dtest=com.logicaalternativa.examples.akka.bus.DeadLettersTest test

Un ‘dead letter‘ se produce cuando no se ha podido entregar un mensaje a un actor. Un caso típico es cuando el actor receptor del mensaje ya no existe. El ‘dead letter‘ contiene el mensaje enviado, la referencia al actor que lo envía y la del que lo iba a recibir.

Por defecto Akka informa en el log de la aplicación los dead letters que se producen (esto es configurable). También pública esta información a través del bus de eventos de Akka, lo que permite que una aplicación tenga la posibilidad de una gestión específica de estos mensajes perdidos.

En el ejemplo para poder provocar un dead letter se crea un actor del tipo ActorNoTypedDummy pararlo para a continuación enviarle un mensaje. Este mensaje ‘se pierde‘ y es recogido como ‘dead letter‘.

Por otra parte en el ejemplo se está utilizando un actor del tipo ActorNoTypedLogEvent configurado para que suscriba en el bus de eventos de Akka los mensajes del tipo DeadLetter.

Cuando se manda un mensaje de ‘lastMessage‘ al actor ActorNoTypedLogEvent este devolverá la información del mensaje perdido en forma de DeadLetter.

Como comprobación final del test se validará la información contenida en el objeto DeadLetter: el mensaje y la referencia al actor receptor.


Hasta aquí este segundo artículo de Akka por ejemplos. El próximo y último artículo de esta serie será sobre actores tipados.

Espero que os sirva.

M.E.

Akka por ejemplos

“Akka representa muchos conceptos nuevos que al principio cuesta asimilar. Para poder entenderlo mejor, he ido creando, poco a poco, una serie de ejemplos que me han permitido comprender mejor el paradigma que conlleva.

[Akkapor ejemplos]

Este artículo trata sobre esto, intentar tener una prueba de código de cada nuevo concepto que permita ver cual es su comportamiento y función.”

Entrada siguiente: Akka por ejemplos. Sobre supervisión, bus de eventos, …

Akka es un framework basado en el modelo de actores que permite dar solución al problema de la programación concurrente. Además de formar parte de Scala de manera nativa existe como librería en Java.

Permite diseñar aplicaciones que sean escalables horizontalmente de manera transparente, con alta toleracia a fallos y basada en eventos que permite cumplir el ‘manifiesto reactivo‘.

Este será el primer de una seríe de artículos explicando conceptos clave que se utilizan en Akka mediante ejemplos. En esta primera parte trataré como funciona las colas de mensajes de un actor, que es un router y las diferentes maneras de como se pueden enviar mensajes entre actores.

En próximas entregas trataré sobre el sistema de supervisión de los actores en Akka, como funciona el bus de eventos y que son los actores tipados.

Esta entrada es eminentemente práctica, te invito a descargarte el proyecto y a que eches un vistazo y ejecutes los ejemplos. Se entenderá mejor lo que quiero explicar en este artículo. Todo el código fuente relacionado con esta serie de post está disponible en mi repositorio de GitHub

[Logo GitHub] AkkaActorsAndFutures

Sobre los test

La idea fundamental a la hora de realizar los test ha sido la de realizar implementaciones dummy y ‘ad hoc‘ y, gracias a ellas, montar test que permitieran ver con facilidad cual es el funcionamiento de la pieza que interesa.

Por otra parte, he buscado que el código incluido en los test puedan servir de ejemplo uando se quiera implementar ese mismo concepto en una aplicación real.

Es por esto por lo que he evitado utilizar el framework de Akka de test ‘akka-testkit’ y en algunos momentos puede parecer que me he complicado la vida realizando las validaciones cuando se completan los futuros y utilizando agentes de Akka para compartir estados entre actores.

Mucha de la funcionalidad común a todos los actores, como crear el sistema de actores y métodos de utilidad se ha llevado a test base (TestBaseTestBaseTypedActor ) de los que heredan todos los test.

Prólogo: Los agentes

Ya que en los test utilizo agentes, antes de entrar a explicar los test, intentaré hacer una breve introdución de lo que es un agente en Akka.

Un agente de Akka permite almacenar un valor y actualizarlo sin problemas de concurrencia. Tienen la particularidad que la actualización se realiza de manera asíncrona, pero se puede leer el valor de manera síncrona.

Su funcionamiento es similar a un mailbox de un actor. Se basa en encolar los nuevos valores de tal manera que el valor interno se va actualizando en orden de llegada. No existe concurrencia y por eso su actualización es asíncrona.

Hay que tener en cuenta que cuando se pide su valor de manera síncrona devuelve el valor ‘actual‘ no el valor ‘futuro‘ que tendrá cuando se procesen todos los valores encolados hasta ese momento.

Vamos entonces con los test.


Como funciona la cola de mensajes de un Actor

Test: ActorQueueTest.java

Commando maven:
mvn  −Dtest=com.logicaalternativa.examples.akka.queue.ActorQueueTest test

[Un actor, un mailbox]

Un actor, un mailbox

El modelo de actores nos asegura que no puede haber más de una ejecución concurrente de un actor. Es decir, no puede haber más de un hilo ejecutando el código de actor. Los mensajes que se envían al actor se encolan en su ‘inbox‘ de tal manera que se ejecutan de uno en uno.

En este ejemplo se está levantando dentro del sistema de actores, un actor del tipo ActorNoTypedDummy. Este tiene una lógica muy sencilla: si se envía un mensaje tipo Long la ejecución se espera los milisegundos indicados en el valor del mensaje. Si el mensaje es de tipo String devuelve un eco del mensaje.

En el test ActorQueueTest se envía un primer mensaje tipo Long para que se realice una espera y después 10 mensajes de tipo String.

En en la ejecución del test, se puede observar de que aunque se envían todos los mensajes sin esperar la ejecución del actor, en realidad estos se encolan. El actor no procesa el siguiente mensaje hasta que no ha terminado la ejecución anterior.

El resultado es que los mensajes son procesados por orden de llegada ya que este actor tiene la implementación por defecto de ‘mailbox‘ (existen otras implementaciones que permiten prioridades de mensajes). Con lo que realiza la espera del primero de ellos y después el eco del resto.

En este test para comprobar el orden de los mensajes que se están ejecutando en el actor se está utilizando un agente de Akka para guardar el estado de manera asíncrona.  El agente almacena la concatenación de los mensajes tipo cadena que se envían al actor.

Para verificar que el orden de los mensajes enviados es el mismo orden de los mensajes que se han procesado se comprueba que estas dos cadenas son iguales.

Cómo funciona un Router

Test: ActorQueueRouteRoundRobinTest.java

Commando maven
mvn  −Dtest=com.logicaalternativa.examples.akka.queue.ActorQueueRouteRoundRobinTest test

Akka permite el concepto de ‘Router‘. Esto permite asociar en una misma referencia de un actor envolver un grupo de actores del mismo tipo.

Se basa en el mismo concepto que tenemos todos en mente de que es un Router: un frontal al que se envían los mensajes y que se encarga de reenviarlos a sus ‘routees‘ para que estos los procesen y devolver la respuesta.

Permite un procesamiento en paralelo mayor, al existir más instancias de un actor, pero con la comodidad de tener una referencia única a la que enviar los mensajes.

[Concepto de router]

Router

Cada miembro del router es de hecho un actor y tiene su propia cola. La estrategia del envío de los mensajes a los miembros del enrutador se define al crear este. En el test de ejemplo se está utilizando la estrategia de envío secuencial de mensajes (‘RoundRobinPool’): se envía un mensaje a un actor del grupo, el siguiente al siguiente actor y así sucesivamente. En la documentación de Akka puedes ver que existen y se pueden utilizar otras estrategias.

El ejemplo levanta un sistema de actores. Se crea un router de 5 actores del tipo ActorNoTypedWhoIam. Este actor tiene una implementación muy sencilla. Se limita a responder en un Map con el mismo mensaje que le envían y su nombre de actor.

Al router se le envían 10 mensajes. En la resolución de cada futuro de respuesta se comprueba que el mensaje es el mismo que se ha enviado. Por otra parte, gracias a la nombre del routee, se guarda la información de que routee ha procesado el mensaje.

Por último se comprueba que a cada actor del grupo del router le ha llegado el mismo número de mensajes (estrategía de envío RoundRobinPool). En este caso son 2 ya que el router lo forman 5 actores y se mandan 10 mensajes.

En las trazas se puede comprobar que el actor con el nombre ‘a‘ le llegan los mensajes ‘0‘ y ‘5‘, al ‘b‘ el ‘1‘ y el ‘6‘ y así sucesivamente.

Como se pueden enviar mensajes entre actores

Test: ActorProxyTest.java

Comando maven
mvn  -Dtest=com.logicaalternativa.examples.akka.message.ActorProxyTest test

Fundamentalmente los actores se pueden enviar mensajes de dos maneras diferentes: tipo ‘fire & forget‘ (dispara y olvida) utilizando la función ‘tell‘ o bien seguir un esquema petición-respuesta utilizando la función ‘ask‘.

Por otra parte un actor puede crear otros actores a los que necesita enviar mensajes. Es muy común que existan actores cuya tarea sea la de orquestar o supervisar el trabajo de los actores que ha creado (‘actores hijos’), y que una parte de esta tarea sea la de reenviar a sus hijos los mensajes que les llegan. Para estos cosos es muy útil utilizar la función ‘forward

El ejemplo se crea un actor ActorNoTypedProxy que crea un actor hijo del tipo ActorNoTypedDummy. Recordemos que este último devuelve un echo de los mensajes que se le envían.

ActorNoTypedProxy hará de proxy para mostrar las diferentes formas de enviar y reenviar mensajes entre actores. Recibe los siguientes mensajes tipo cadena:

forward‘, redirigir el mensaje

Commando maven test 'forward'
mvn -Dtest=com.logicaalternativa.examples.akka.message.ActorProxyTest#testForward test

Cuando recibe un mensaje de este tipo ejecuta llamada al método ‘forward‘ del actor hijo, con el mismo mensaje. El actor hijo (ActorNoTypedDummy) recibirá el mensaje como si el emisor hubiera sido el que emitió el mensaje al actor ActorNoTypedProxy.

redirect‘ dispara y olvida

Comando maven test redirect
mvn -Dtest=com.logicaalternativa.examples.akka.message.ActorProxyTest#testRedirectMessageToChild test

El actor que hace de proxy redirecciona el mensaje llamando a la función ‘tell‘ del hijo indicando que el remitente es el del mensaje original que le llega al proxy. Aquí también el actor hijo (ActorNoTypedDummy) recibirá el mensaje como si el emisor hubiera sido el que emitió el mensaje a ActorNoTypedProxy.

[forward o 'redirect']

forward o ‘redirect’

 ‘future‘, petición-respuesta no bloqueante y asíncrona

Comando maven test 'future'
mvn -Dtest=com.logicaalternativa.examples.akka.message.ActorProxyTest#testFutur test

Este es un ejemplo de comportamiento no bloqueante y asíncrono utilizando los futuros de Scala que ofrece Akka. ActorNoTypedProxy utiliza la función ‘ask‘ para con el mismo mensaje que le ha llegado, preguntar al actor hijo ActorNoTypedDummy y obtener el futuro de la respuesta. Este futuro se envía al remitente del mensaje original.

Al ser este un futuro, si fuera necesario, se puede desde el actor que hace de proxy, operar con él o realizar cualquier tipo de transformación. Por ejemplo se puede registrar un callback, realizar alguna transformación operando con el de manera funcional, etc…

Para simplificar en el test se espera el resultado de la respuesta y como esta también es un futuro, se espera también a este para obtener el resultado.

 ‘await‘, petición-respuesta bloqueante y síncrona

Comando maven test 'await'
mvn -Dtest=com.logicaalternativa.examples.akka.message.ActorProxyTest#testAwaitFutur test

Sigue el mismo esquema que el test anterior pero esperado la respuesta de una manera síncrona.

En el actor proxy realiza la misma llamada ‘ask‘ y a continuación se utiliza la función ‘Await‘ que espera la resolución del futuro. Se obtiene así un comportamiento bloqueante y síncrono de petición-respuesta.


Hasta aquí llega esta primera parte de esta serie de artículos centrados en explicar como funciona Akka con ejemplos. Conceptos como la supervisión entre actores, el bus de eventos y que son los actores tipados serán material de futuras entregas.

Espero que os sirva.

M.E.

Entrada siguiente: Akka por ejemplos. Sobre supervisión, bus de eventos, …