Cuando gestionamos eventos, en Elixir existen distintas posibilidades para construir nuestros sistemas, desde el viejo conocido gen_event, al nuevo gen_stage o al sencillo phoenix_pubsub. Pero, ¿sabrías decir para qué sirve cada uno, cuales son sus puntos fuertes y sus puntos débiles?

El capítulo 10 del libro Elixir/OTP: Alquimia con OTP explora el uso de GenStage, cuando estaba escribiéndolo me vino a la cabeza la gestión de eventos y las diferentes formas que hay en Elixir para poder implementar sistemas de gestión de eventos. Los hay tan potentes como phoenix_pubsub, simples como gen_event y completos como gen_stage.

En un principio gen_event estuvo disponible como GenEvent en Elixir pero al no aportar nada sobre el comportamiento por defecto disponible en BEAM se terminó marcando como desfasado y en una futura versión de Elixir desaparecerá. Pero, ¿por qué en Elixir este comportamiento no se tomó para la gestión de eventos y sin embargo desde Elixir se desarrolló GenStage y desde Phoenix se desarrolló PubSub?

Las carencias de :gen_event

Uno de los motivos por los que :gen_event fue tratado como un mal patrón y fue descartado de Elixir fue porque no es realmente concurrente. Puedes ver en el capítulo 6 del libro Erlang/OTP Volumen II: Las Bases de OTP cómo funciona este patrón. Cada manejador se agrega como función a un único proceso y cuando pedimos al comportamiento notificar un evento, el proceso toma el listado de manejadores y ejecuta uno a uno de forma secuencial todos los manejadores.

Algunos problemas que presenta este patrón:

  1. Todo sucede en un proceso. No existe concurrencia y los manejadores son funciones llamadas desde un único proceso.
  2. Es un cuello de botella potencial.
  3. Si un manejador falla es eliminado del listado sin posibilidad de reconectarlo, no podemos supervisar una función.

Como vemos, tiene importantes carencias. No obstante, es un comportamiento implementado en Erlang desde hace muchos años y se ha empleado, ¿cómo podríamos emplearlo y sacarle partido?

  • Implementando los manejadores como llamadas asíncronas a un servidor genérico. El servidor puede fallar y no afectaría al manejador además de que el servidor sí podría estar bajo un árbol de supervisión. Además, si realizamos simples cast el manejador y el gestor de eventos actúan a modo de balanceador de carga pudiendo despachar mucho más rápido los mensajes.
  • Mantener simple y a libre de fallos la función a ser agregada como manejador si no puede ser un envío asíncrono a un servidor genérico.

Con estas precauciones puedes seguir empleando este comportamiento. No obstante, hay que reconocer que este comportamiento se empleó para muchos elementos de Erlang y en versiones recientes han sido reescritos con otras implementaciones diferentes. Un ejemplo es logger, el nuevo sistema de anotaciones de Erlang (capítulo 10 de Erlang/OTP Volumen II) y traído desde Elixir con ideas de sistemas implementados en Erlang de hace mucho tiempo como lager.

Entonces, ¿recomendaría el uso de este comportamiento? Realmente si tu código es muy simple y no tendrá necesidades muy elevadas de concurrencia o alta carga puede ser una opción a tener en cuenta. Es un comportamiento sencillo y elimina mucho código a implementar para cuando queremos procesar un evento a través de diferentes manejadores configurables.

Hacia los 2 millones, Phoenix.PubSub

El desarrollo de Phoenix Framework ha estado envuelto en muchas pruebas e implementaciones en grandes empresas. Tras algunos episodios de implementar elementos necesarios como Registry, llegó Phoenix.PubSub permitiendo la distribución de eventos entre los procesos suscritos localmente y el envío de estos eventos también a otras máquinas conectadas donde también haya un proceso pubsub ejecutándose.

La potencia de Phoenix.PubSub es patente. Ha demostrado ser útil y funcionar para la publicación y consumición de eventos en situaciones de alta carga y nos garantiza la escalabilidad sin mucho dolor. Podríamos decir que este sistema presenta carencias, pero está muy bien diseñado y es extensible permitiéndonos implementar nuestro propio despachador. En comparación con gen_event este sistema se nota superior y proporciona las dinámicas necesarias para facilitar su uso al máximo.

Por otro lado, sistemas como GenStage son mucho más completos y en comparación hacen ver la falta de elementos por defecto en Phoenix.PubSub que sí están disponibles en GenStage. Por ejemplo, un control de la demanda de los mensajes no recibiendo ningún mensaje hasta estar preparados para obtenerlos.

La potencia de GenStage

El sistema GenStage estuvo en la mente de José Valim durante bastante tiempo, al principio lo llamó GenRouter y luego pasó a ser GenBroker, para finalmente quedarse como GenStage. Este sistema es muy potente porque permite generar procesos para actuar de diferentes formas:

  • Productores. Generando demanda a mendida que se solicite o recibiendo información para transformarla en eventos y enviarla a los consumidores.
  • Consumidores. Suscribiéndose a uno o varios productores, indicando la demanda mínima y máxima soportada o control manual sobre la demanda.
  • Productores/Consumidores o Almacenes. Obtienen eventos de uno o varios productores y los ponen a disposición de consumidores conectados a ellos para obtener estos eventos procesados, acumulados o filtrados, depende de qué queramos hacer con ellos en el almacén.

Uno de los principales problemas a la hora de utilizar GenStage es la cantidad de características que presenta. Puede llegar a ser abrumador y por tanto decidir emplear otra opción más simple. Sin embargo, una vez se tiene un control más fino del comportamiento, es de agradecer la capacidad de configuración y la flexibilidad de sus características.

Como dije antes, el capítulo 10 de Elixir/OTP habla en detalle de cada uno de los aspectos de este comportamiento, si quieres saber más sobre cómo utilizarlo, ya puedes adquirir el libro y sacarle partido a OTP en Elixir.

¿Entonces cuál uso?

Como he mencionado en el último apartado, lo ideal es emplear GenStage cuanto más mejor. Sus características hacen difícil que nuestro caso de uso no pueda ser implementado y en caso de necesitar ajustar su uso, rendimiento o realizar trazas, es el comportamiento más simple para poder realizar estas tareas.

No obstante, si no tenemos aún soltura con este comportamiento y necesitamos implementar algo rápido sin complicaciones y no tendrá mucho impacto en nuestro sistema, podemos optar por phoenix_pubsub o incluso hacer una implementación muy rápida con :gen_event y evitar así las dependencias. Todo depende de nuestros requisitos. Espero os haya sido de ayuda, no olvides dejar un comentario y gracias por leer.

Comentarios

No hay comentarios. Inicia sesión para enviar un comentario.