OTP Gen Server

Материал из Erlang по-русски.

Оригинал этой статьи находится по адресу Gen_Server Behaviour

Автор: Didro
Дата: 29.10.2006
Версия: 1.0



Содержание

Паттерн поведения Gen_Server (Generic Server)

Данный раздел представляет собой обзор серверной части клиент-серверного взаимодействия, реализуемого на основе OTP. Детальное описание интерфейса обобщённого сервера и callback-функций, предоставляемых им для реализации, приведено в описании модуля gen_server.

Клиент-серверная модель взаимодействия

В рамках клиент-серверной модели существует один сервер и произвольное число клиентов. Эта модель в основном используется при проектировании управления ресурсами, когда нескольким клиентам требуется доступ к общему ресурсу. Сервер хранит этот ресурс и управляет доступом к нему. Image:clientserver.gif

Пример

Пример простого сервера, написанного на чистом Erlang (без использования OTP) приведён во введении Введение. Используя шаблон поведения gen_server, описанный в одноимённом модуле, и реализуя callback часть сервера, получим:

-module(ch3).
-behaviour(gen_server).
 
-export([start_link/0]).
-export([alloc/0, free/1]).
-export([init/1, handle_call/3, handle_cast/2]).

start_link() ->
   gen_server:start_link({local, ch3}, ch3, [], []).

alloc() ->
   gen_server:call(ch3, alloc).

free(Ch) ->
   gen_server:cast(ch3, {free, Ch}).

init(_Args) ->
   {ok, channels()}.

handle_call(alloc, _From, Chs) ->
   {Ch, Chs2} = alloc(Chs),
   {reply, Ch, Chs2}.

handle_cast({free, Ch}, Chs) ->
   Chs2 = free(Ch, Chs),
   {noreply, Chs2}.

Запуск Gen_Server

В предыдущем примере функцией запуска gen_server является функция ch3:start_link():

start_link() ->
   gen_server:start_link({local, ch3}, ch3, [], []) => {ok, Pid}

Функция start_link производит вызов функции gen_server:start_link/4, которая порождает новый процесс и связывает его с gen_server’ом.

  • Первый аргумент – {local, ch3} – определяет имя сервера. В нашем случае сервер будет локально зарегистрирован под именем ch3. Если имя будет опущено, то gen_server не будет зарегистрирован. Вместо имени будет использоваться идентификатор процесса. В качестве имени также может быть передан кортеж {global, Name}, при этом сервер будет зарегистрирован с помощью функции global:register_name/2.
  • Второй аргумент – ch3 – имя callback-модуля, в котором расположены callback-функции, реализованные нами. В нашем случае интерфейсные функции (start_link, alloc и free) и callback- функции (init, handle_call и handle_cast) расположены в одном модуле. Это достаточно обычная ситуация, поскольку удобно размещать код, связанный с процессом, в одном модуле.
  • Третий аргумент – [] – терм, который передаётся в качестве параметра callback-функции init. В нашем случае init не требуется никаких входных данных, поэтому мы передаём третьим аргументом пустой список.
  • Четвёртый аргумент – [] – список опций. Список доступных опций приводится в описании модуля gen_server.

В случае успешной регистрации созданный серверный процесс вызывает callback-функцию ch3:init([]). Предполагается, что init вернёт кортеж {ok, State}, где State – внутреннее состояние сервера. В нашем случае таким состоянием является множество доступных соединений – channels.

init(_Args) ->
   {ok, channels()}.

Вызов функции gen_server:start_link является синхронным. Управление не будет возвращено до тех пор, пока не будет завершена инициализация сервера, и он не будет готов принимать входящие запросы. Функция gen_server:start_link должна быть использована при создании сервера как части дерева супервизоров, в этом случае сервер должен быть запущен супервизором. Существует и другая функция – gen_server:start – для запуска автономного, т.е. не являющегося частью дерева супервизоров сервера.

Синхронный вызов – Call

Возможность посылки синхронного запроса alloc() серверу реализована с помощью функции gen_server:call/2:

alloc() ->
   gen_server:call(ch3, alloc).

ch3 – имя сервера (должно совпадать с именем, под которым сервер был запущен). alloc – само значение («текст») запроса. Запрос превращается в сообщение и посылается серверу. При получении этого сообщения gen_server вызывает функцию handle_call(Request, From, State). Предполагается, что эта функция вернёт кортеж {reply, Reply, State1}, где Reply – ответ, передаваемый клиенту, а State1 – новое внутренне состояние сервера.

 handle_call(alloc, _From, Chs) ->
    {Ch, Chs2} = alloc(Chs),
    {reply, Ch, Chs2}.

В нашем случае ответом является выделенное соединение, а новым состоянием сервера – Chs2 – список оставшихся, незанятых соединений. Таким образом, вызов функции ch3:alloc() возвращает выделенное соединение и обновляет список доступных соединений. Затем сервер продолжает ожидать поступления новых запросов.

Асинхронный вызов – Cast

Возможность асинхронного запроса free(Ch) реализована с помощью функции gen_server:cast/2:

free(Ch) ->
   gen_server:cast(ch3, {free, Ch}).

ch3 – имя сервера. {free, Ch} – «текст» запроса. Запрос превращается в сообщение и посылается серверу. Сразу после этого функция free(Ch) возвращает результат ok и выполнение клиентского процесса продолжается. При получении сообщения сервер вызывает функцию handle_cast(Request, State). Предполагается, что эта функция вернёт кортеж {noreply, State1}. Где State1 – новое состояние сервера.

handle_cast({free, Ch}, Chs) ->
   Chs2 = free(Ch, Chs),
   {noreply, Chs2}.

В нашем случае новым состоянием является обновлённый список доступных соединений Chs2. Теперь сервер готов к обработке новых запросов.

Остановка

Сервер – часть дерева супервизоров

Если сервер является частью дерева супервизоров, то необходимость в отдельной функции завершения работы отсутствует, поскольку сервер будет автоматически остановлен своим супервизором. Конкретный способ остановки определяется стратегией завершения работы потомка супервизора. Если необходимо произвести некоторую очистку перед завершением работы, то стратегия завершения должна быть определена как значение тайм-аута, кроме того, у процесса gen_server должен быть установлен флаг обработки сигнала exit. Установку флага можно осуществить в функции init.

init(Args) ->
   ...,
   process_flag(trap_exit, true),
   ...,
   {ok, State}.

При получении команды на завершение работы сервер вызывает callback-функцию terminate(shutdown, State):

terminate(shutdown, State) ->
   ..code for cleaning up here..
   ok.

Автономный сервер

Если сервер не является частью дерева супервизоров, то функция завершения работы может оказаться полезной, например:

...
export([stop/0]).
...
stop() ->
   gen_server:cast(ch3, stop).
...
handle_cast(stop, State) ->
   {stop, normal, State};
handle_cast({free, Ch}, State) ->
   ....
terminate(normal, State) ->
   ok.

В данном примере callback-функция, обрабатывающая запрос stop, возвращает кортеж {stop, normal, State1}, где значение normal говорит о корректном завершении работы, а State1 содержит новое состояние сервера. Это позволит gen_server’у вызвать функцию terminate(normal,State1) и корректно завершить исполнение.

Перехват других сообщений

Если сервер может получать другие сообщения, отличные от вызовов-запросов, то для их обработки требуется реализовать callback-функцию handle_info(Info, State). Примером таких сообщений может служить сообщение exit, получаемое процессом в случае, если он связан с другими процессами и обрабатывает сигнал exit (последнее устанавливается с помощью специального флага).

handle_info({'EXIT', Pid, Reason}, State) ->
   ..code to handle exits here..
   {noreply, State1}.