aprendiendo ( Erlang ).

miércoles, 16 de noviembre de 2011

gen_server (Servidor genérico)

| 1 comentarios |

El modelo cliente-servidor se caracteriza por un servidor central y un número indeterminado de clientes. El modelo cliente-servidor se utiliza generalmente para las operaciones de gestión de recursos, donde los clientes pueden compartir recursos y el servidor se encarga de gestionar estos recursos.

Erlang/OTP implementa un servidor genérico en el módulo gen_server (generic server). El módulo gen_server implementa un conjunto de funciones, que nos asegura las siguientes características:
  1. División entre la funcionalidad y la operativa de servidor.
  2. Traza y reporte de errores. Permite ser linkado y atrapar la salida.
  3. Intercambio de código en caliente.

Un servidor genérico, gen_server asume que una parte específica de la funcionalidad, esta implementada en un módulo que a partir de ahora llamaremos callback. El módulo callback se encargará de implementar un interfaz para el usuario del servidor, que funcionará de puente con el gen_server. Y a su vez, el gen_server se encargará de ejecutar la funcionalidad correspondiente en el callback.

Y, ¿qué quiere de decir esto? supón por un momento que existe una implementación de un ridículo servidor de manidos hola mundo's, que llamaremos originalmente hola_server. Bueno, pues como usuario final de dicho servidor hola_server necesito una función que me permita arrancar el servidor, una para saludar y otra para despedirme. Pues bien, este será el interfaz que deberá implementar el callback del servidor hola_server. La implementación de estas funciones deberán pedir al gen_server que realice dicha operativa y el gen_server se encargará de ejecutar la función que imprime el hola mundo.

El conjunto de relaciones que existen entre las funciones del gen_server y el módulo callback viene dado por la siguiente tabla:
Módulo gen_server Módulo callback
gen_server:start
gen_server:start_link
Module:init/1
gen_server:call
gen_server:multi_call
Module:handle_call/3
gen_server:cast
gen_server:abcast
Module:handle_cast/2
Module:handle_info/2
Module:terminate/2
Module:code_change/3
Una llamada a una función XXXXX del gen_server le corresponde una ejecución YYYYY en el callback

Funciones del módulo gen_server

Veamos la funciones que nos proporciona el módulo gen_server.

start_link/3,4
Arranca un servidor genérico, linkándolo, como parte de un árbol de supervisión. Esta función asegura que el proceso será linkado al supervisor.
  • start_link(Modulo_callback, Args, Opciones) -> Resultado
  • start_link(Nombre_servidor, Modulo_callback, Args, Opciones) -> Resultado

El servidor genérico llama, de forma síncrona, al Modulo_callback:init/1 para inicializarse. Esta función retorna el resultado de llamar al Modulo_callback:init/1 con los argumentos Args.

Si llamamos con Nombre_servidor={local,Nombre} el gen_server se registrará localmente, usando register/2, con el nombre Nombre. Si Nombre_servidor={global,Nombre} el gen_server se registrará globalmente, usando global:register_name/2, con el nombre Nombre. Si no se establece Nombre_servidor el servidor genérico no se registrará, es decir, será un proceso no registrado.

Las Opciones son un conjunto de parámetros que podemos establecer en el servidor como: timeout, modo de depuración, etc ... Para más información consultar la documentación.

El resultado de la función puede ser {ok,Pid} | ignore | {error,Error}. Si el arranque del servidor ha sido satisfactorio entonces retornará {ok,Pid}, donde Pid es el pid del servidor. Si el servidor es un servidor con nombre, es decir, utilizamos la función gen_server:start_link/4 para arrancarlo, entonces puede retornar {error,{already_started,Pid}}, donde el Pid es el pid del proceso registrado. Si el arranque del servidor falla en el Modulo_callback:init/1, este debe retornar {stop, Motivo} | ignore, en cualquier caso el servidor no arrancará y retornará {error, Motivo} | ignore.

start/3,4
El funcionamiento y comportamiento de esta función es idéntico al start_link/3,4, con la diferencia de que el proceso creado no es linkado.
  • start(Modulo_callback, Args, Opciones) -> Resultado
  • start(Nombre_servidor, Modulo_callback, Args, Opciones) -> Resultado

call/2,3
Implementación de un servicio síncrono, en definitiva, el cliente realiza un petición al servidor esperando una respuesta.
  • call(RefServidor, Peticion) -> Respuesta
  • call(RefServidor, Peticion, Timeout) -> Respuesta
El servidor genérico llama al Modulo_callback:handle_call/3 para resolver la petición, retornando la respuesta a la petición.

La referencia al servidor (RefServidor) puede ser:
  • Pid: el pid del servidor.
  • Nombre: el nombre con el que se registro localmente.
  • {Nombre, Nodo}: cuando se registro localmente en otro nodo.
  • {global, Nombre}: cuando se registro globalmente.
El Timeout por defecto es de 5000 Milisegundos.

multi_call/2,3,4
Realiza una llamada síncrona a todos los servidores genéricos registrados en con el nombre Nombre o los que estén registrados en un conjunto de nodos.
  • multi_call(Nombre, Peticion) -> Resultado
  • multi_call(Nodos, Nombre, Peticion) -> Resultado
  • multi_call(Nodos, Nombre, Peticion, Timeout) -> Resultado
La función retorna una tupla con el formato {Respuestas, Nodos}, donde las Respuestas es una lista de tuplas con el nodo y la respuesta del nodo y Nodos es la lista de nodos que no existen, o no existe el servidor o no ha respondido.

El servidor genérico llama al Modulo_callback:handle_call/3, del mismo modo que call/2,3, de cada servidor genérico para componer la lista de respuestas.

cast/2
Realiza una petición asíncrona, no espera respuesta, retornando inmediatamente un ok sin importar que la petición tenga éxito o no. El funcionamiento es similar al call/2,3.
  • cast(RefServidor, Peticion) -> ok
El servidor genérico llama al Modulo_callback:handle_cast/2 para resolver la petición.

abcast/2,3
Comportamiento similar al multi_call/2,3,4, con la diferencia de tratarse de una petición asíncrona.
  • abcast(Nombre, Peticion) -> abcast
  • abcast(Nodos, Nombre, Peticion) -> abcast

reply/2
Se utiliza para enviar una respuesta a un cliente.
  • reply(Cliente, Respuesta) -> Resultado

enter_loop/3,4,5
Convierte un proceso existente en un servidor genérico. No retorna, sino que el proceso de llamada comienza a recibir peticiones. El programador se hace responsable de iniciar el proceso y registrarlo si es necesario.
  • enter_loop(Modulo_callback, Opciones, Estado)
  • enter_loop(Modulo_callback, Opciones, Estado, Nombre_servidor)
  • enter_loop(Modulo_callback, Opciones, Estado, Timeout)
  • enter_loop(Modulo_callback, Opciones, Estado, Nombre_servidor, Timeout)
Esta función es útil cuando un servidor necesita un sistema de arranque muy complejo.

Los parámetros de estas funciones son idénticos a los de start_link/3,4 y start/3,4, con excepción del Estado ya que el nuevo servidor no llamará a la función Modulo_callback:init/1, por lo que hay que indicarle el estado inicial.


Funciones del módulo callback

Veamos que estamos obligado a implementar en nuestro módulo callback.

init/1
Función encargada de inicializar el servidor genérico y determinar el estado inicial.
  • init(Args) -> Resultado
Como ya hemos comentado cuando un gen_server arranca con las funciones start/3,4 o start_link/3,4 se encarga de llamar a la función de inicialización del servidor. Los argumentos recibidos en Args son los argumentos pasados a la función de arranque del servidor.

Si el arranque del servidor ha sido correcto el resultado de la función será {ok, Estado} | {ok, Estado, Timeout} | {ok, Estado, hibernate}. Si establecemos un valor para el Timeout determinaremos el timeout del la recepción de mensajes en el servidor. Si nos determinamos por hibernar el servidor, el servidor permanecerá hibernado hasta que llegue el primer mensaje.

Si la función devuelve {stop, Motivo} | ignore detendremos el servidor.

handle_call
Cuando un servidor genérico recibe una petición enviada usando gen_server:call/2,3 o gen_server:multi_call/2,3,4 se llama al manejador de la respuesta para que resuelva la petición.
  • handle_call(Peticion, Desde, Estado) -> Resultado
Si la función retorna {reply, Respuesta, Nuevo_Estado}, {reply, Respuesta, Nuevo_Estado, Timeout} o {reply, Respuesta, Nuevo_Estado, hibernate} indicamos al servidor genérico que todo ha concluido correctamente, que el nuevo estado del servidor Nuevo_Estado y que la respuesta a la petición es Respuesta.

Si la función retorna {noreply, Nuevo_Estado}, {noreply, Nuevo_Estado, Timeout} o {noreply, Nuevo_Estado, hibernate}, el gen_server continuará ejecutándose con el Nuevo_Estado.

Si la función devuelve {stop, Motivo, Respuesta, Nuevo_Estado} o {stop, Motivo, Nuevo_Estado}, el gen_server entonces llamará al Modulo_callback:termitate(Motivo, Nuevo_Estado) y terminar.

handle_cast
Cuando un servidor genérico recibe una petición enviada usando gen_server:cast/2 o gen_server:abcast/2,3 se llama al manejador de la petición Modulo_callback:handle_cast/2.
  • handle_cast(Peticion, Estado) -> Resultado
El comportamiento es similar al de handle_call salvo por que no calcula respuesta.

info
Esta función es llamada por el servidor genérico cuando se produce un timeout o cuando se recibe un mensaje que no sea síncrono o asíncrono (que no sean tratados con handle_call o handle_cast).
  • handle_info(Info, Estado) -> Resultado
Puede retornar el mismo Resultado que una función handle_cast.

terminate
Esta función es llamada por el gen_server cuando llega el momento de finalizar la ejecución. Si en la función Modulo_callback:init/1 tiene la misión de inicializar el servidor, la función Modulo_callback:terminate/2 tiene la obligación de liberar los recursos.
  • terminate(Motivo, Estado)
El Motivo puede ser [normal | shutdown | {shutdown, término} | termino].

code_change
Esta función es llamada por el servidor genérico cuando se realiza un cambio de versión en caliente ya sea para un upgrade como para un downgrade.
  • code_change(Version_antigua, Estado, Extra) -> {ok, Nuevo_Estado}
Sobre este tema hablaremos largo y tendido en otro post. Pero si necesitáis más podéis encontrar información en documentación oficial de gen_server.

format_status
Esta función es opcional, por lo que el módulo callback no tienen necesidad implementarla. El módulo gen_server proporciona una implementación predeterminada de esta función que devuelve el estado.
  • format_status(Opcion, [PDict, Estado]) -> Estado
Esta función es invocada: cuando se llama a la función sys:get_status/1,2, el parámetro Opcion se establece a normal; o cuando el servidor termina de forma anómala realiza un log del error, el parámetro Opcion se establece a terminate.

Si necesitáis profundizar documentación gen_server.

Hola server. Primeros pasos:

Pues sí. He realizado ese pedazo de servidor de saludos y despedidas. Con él, sólo pretendo mostrar que todo lo expuesto es más simple de lo que parece.

Estos son los pasos que he seguido para realizar el servidor:
  1. Ir al menú mi Emacs Erlang/Skeletons/gen_server para obtener un template de un servidor genérico. He quitado los comentarios para que se vea mejor el código y no emborrone.
  2. Definir el interfaz de nuestro servidor, es decir, las funciones hola/1 y adios/1
  3. Implementar los manejadores handle_call correspondiente a cada llamada call

-module(hola_server).

-behaviour(gen_server).

%% API
-export([start_link/0]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3, hola/1, adios/1]).

-define(SERVER, {local, ?MODULE}).

-record(state, {}).

start_link() ->
    gen_server:start_link(?SERVER, ?MODULE, [], []).

hola(Nombre) ->
    gen_server:call(?MODULE, {hola, Nombre}).

adios(Nombre) ->
    gen_server:call(?MODULE, {adios, Nombre}).

init([]) ->
    {ok, #state{}}.

handle_call({hola, Nombre}, _From, State) ->
    io:format("Hola ~p~n", [Nombre]),
    Reply = ok,
    {reply, Reply, State};
handle_call({adios, Nombre}, _From, State) ->
    io:format("Adios ~p~n", [Nombre]),
    Reply = ok,
    {reply, Reply, State}.

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.


1> hola_server:start_link(). {ok,<0.37.0>} 2> hola_server:hola(mundo). Hola mundo ok 3> hola_server:adios(mundo). Adios mundo ok

Y eso es todo no hay más. Un servidor de saludos en dos patadas, lo que llaman por ahí wash & go. Por supuesto, si ya ... es un ejemplo muy chorra pero como ejemplito para toma de contactos no esta mal y te permite experimentar un poco. Seguro que el siguiente ejemplo te parece más interesante.

Servidor de nombres

Compliquemos un poco el tema. En este caso vamos a implementar un servidor de nombres, donde el estado del servidor vendrá dado por un conjunto de nombres, que se construye en la función init/1 del módulo callback nombre_server.

Como interfaz tenemos:
  1. nombre_server:nuevo/1: Inserta un nombre en el registro del servidor de forma síncrona.
  2. nombre_server:borrar/1: Borrar un nombre del registro del servidor de forma asíncrona.
  3. nombre_server:esta/1: Comprueba si un nombre dado esta registrado en el servidor.
  4. nombre_server:listar/0: Lista de forma asíncrona los nombres que tiene registrado el servidor.

Veamos la implementación:
-module(nombre_server).

-behaviour(gen_server).

%% API
-export([start_link/0]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3, nuevo/1, borrar/1, esta/1,
        listar/0]).

-define(SERVER, ?MODULE).


start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

nuevo(Nombre) ->
    gen_server:call(?MODULE, {nuevo, Nombre}).

borrar(Nombre) ->
    gen_server:cast(?MODULE, {borrar, Nombre}).

esta(Nombre) ->
    gen_server:call(?MODULE, {esta, Nombre}).

listar() ->
    gen_server:cast(?MODULE, {listar}).

%%====================================================================
%% gen_server callbacks
%%====================================================================
init([]) ->
    {ok, sets:new()}.

handle_call({nuevo, Nombre}, _From, _State) ->
    io:format("Hola ~p~n", [Nombre]),
    {reply, ok, sets:add_element(Nombre, _State)};

handle_call({esta, Nombre}, _From, _State) ->
    io:format("Esta ~p? ~p~n", [Nombre, sets:is_element(Nombre, _State)]),
    {reply, ok, _State}.

handle_cast({borrar, Nombre}, _State) ->
    State = sets:del_element(Nombre, _State),
    {noreply, State};

handle_cast({listar}, _State) ->
    io:format("~p~n", [sets:to_list(_State)]),
    {noreply, _State}.

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

1> c(nombre_server). {ok,nombre_server} 2> nombre_server:start_link(). {ok,<0.42.0>} 3> nombre_server:listar(). [] ok 4> nombre_server:esta(verdi). Esta verdi? false ok 5> nombre_server:nuevo(verdi). Hola verdi ok 6> nombre_server:esta(verdi). Esta verdi? true ok 7> nombre_server:nuevo(alvaro). Hola alvaro ok 8> nombre_server:nuevo(marta). Hola marta ok 9> nombre_server:esta(alvaro). Esta alvaro? true ok 10> nombre_server:listar(). [marta,verdi,alvaro] ok 11> nombre_server:borrar(verdi). ok 12> nombre_server:listar(). [marta,alvaro] ok

He utilizado funciones cast y call para ilustrar el uso de servicios asíncronos y síncronos. En futura entregas realizaremos ejemplos más complejos y entretenidos.

Publicar un comentario

1 comentarios:

  1. Ricardo dijo...
    Este comentario ha sido eliminado por el autor.
 
Licencia Creative Commons
Aprendiendo Erlang por Verdi se encuentra bajo una Licencia Creative Commons Atribución-NoComercial-CompartirIgual 3.0 Unported.