R. Chavarria's Blog

Proud of developing software, proud of being an Engineer

Elixir: noveno asalto

En el asalto anterior aprendimos un par de conceptos básicos sobre los nodos. En este asalto aprenderemos sobre servidores OTP: qué son, para qué sirven, por qué son útiles y cómo implementarlos fácilmente.

Todo esto, siguiendo el método de aprendizaje con el que comenzé la serie:

  • Aprender lo suficiente para comenzar
  • Experimentar, jugar, buscar puntos desconocidos, hacerse preguntas
  • Aprender lo suficiente para hacer algo de utilidad
  • Enseñar lo aprendido

Aprender lo suficiente para comenzar

Servidores OTP

OTP (Open Telecom Platform) se presenta como la solución a todos tus problemas de escalabilidad y concurrencia. No es así, pero ayuda mucho. Ayuda en temas como descubrimiento de aplicaciones, gestión y detección de fallos, actualización de código en caliente y estructura del servidor.

OTP define un sistema como una jerarquía de aplicaciones. Una aplicación consiste en uno o varios procesos. Cada uno de estos procesos implementa un comportamiento. Existen comportamientos para servidores, gestores de eventos, máquinas de estado finitas, …

Lo implementado en ejercicios anteriores sigue un patrón con el que se podría implementar casi todos los servidores. Por eso, OTP proporciona un mecanismo para liberarnos de escribir el código más tedioso. La librería ofrece unas funciones a modo de callbacks que irá llamando dependiendo de la situación.

Implementar un servidor OTP

1
2
3
4
5
6
7
8
defmodule Sequence.Server do
  use GenServer

  def handle_call(:next_number, _from, current_number) do
    { :reply, current_number, current_number + 1 }
  end

end

use GenServer indica a Elixir que vamos a usar este comportamiento. Así, este módulo representa un servidor OTP.

Uno de los callbacks proporcionados por GenServer es handle_call. Tiene 3 parametros: acción, PID del origen de la petición y el estado actual del servidor. Tiene que responder con una tupla con tres parámetros también: la respuesta, el valor retornado y el estado del servidor a usar en la siguiente llamada.

Para llamar a este servidor, entramos en iex. Arrancamos el servidor, indicando el módulo y el estado inicial (similar a spawn_link).

1
2
3
4
5
6
7
8
promtp$ iex -S mix
iex> { :ok, pid } = GenServer.start_link(Sequence.Server, 100)
iex> GenServer.call(pid, :next_number)
100
iex> GenServer.call(pid, :next_number)
101
iex> GenServer.call(pid, :next_number)
102

Servidores que no tienen que devolver un resultado

En ocasiones no necesitamos que el servidor retorne un resultado. En estos casos, para llamar al servidor emplearemos GenServer.cast, y para manejar esas peticiones, nuestro servidor debe implementar el callback handle_cast.

Callbacks de GenServer

GenServer es un protocolo de OTP. OTP asume que este protocolo define 6 callbacks. Elixir proporciona una implementación por defecto para cada uno de ellos en GenServer, por eso no tenemos que implementarlos nosotros. Los 6 callbacks son: init(start_arguments), handle_call(request, from, state), handle_cast(request, state), handle_info(info, state), terminate(reason, state), code_change(from_version, state, extra) y format_status(reason, [ pdict, state ]).

Nombrado de procesos

En lugar de usar el PID para referenciar a procesos de nuestro servidor, podemos hacerlo a través de nombres. Para ello, se debe utilizar la opción name: a la hora de crear el servidor:

1
2
3
4
5
6
7
8
iex> { :ok, pid } = GenServer.start_link(Sequence.Server, 100, name: :seq)
iex> GenServer.call(:seq, :next_number)
100
iex> GenServer.call(:seq, :next_number)
101
iex> GenServer.call(:seq, :next_number)
102
iex> :sys.get_status :seq

Experimentar, jugar, buscar puntos desconocidos, hacerse preguntas

Aprender lo suficiente para hacer algo de utilidad

  • Ejercicio 01: crear un server que implemente una pila. Se inicializará con unos cuantos valores en la pila. Cada petición pop devolverá un elemento de la pila. Cuando la pila esté vacía, fallará. Implementado en el proyecto mix: code/round-09/stack.

  • Ejercicio 02: ampliar el servidor anterior, de forma que se puedan añadir elementos a la pila con la operación :push a través de peticiones cast. Implementado en el proyecto mix: code/round-09/stack2.

  • Ejercicio 03: dar un nombre al servidor anterior, de forma que se le pueda llamar sin necesidad de saber el PID. También, crear un API en la pila de forma que los clientes no tengan que llamar a GenServer para usarla. Simplemente serán unas funciones que envolverán las llamadas a GenServer. Implementado en otro proyecto mix, en code/round-09/stack3.

  • Ejercicio 04: implementar el callback GenServer.terminate/2 para comprobar distintas formas de terminar el servidor: un callback lanza una excepción, una llamada a Kernel.exit/1, se detecta que un proceso ha tenido un error,…

Resultado

No he obtenido nada en claro. Tendría que profundizar en la documentación de GenServer.terminate/2, que parece bastante espesa por cierto. Pero no está garantizado que se llame a terminate, con lo que no sé si estoy provocando correctamente los errores.

Enseñar lo aprendido, y repetir desde el paso 7

Aquí está, este post, mis notas, mis pensamientos, mis dudas y mi código. Hasta el siguiente asalto.

Referencias

Comments