Pull to refresh

Введение в gen_server: «Erlybank»

Reading time8 min
Views12K
Original author: Mitchell Hashimoto
Предыстория
Введение в Open Telecom Platform/Открытую Телекомуникационную Платформу(OTP/ОТП)

Это первая статья из цикла статей, описывающих все концепции, которые относятся к Erlang/OTP. Чтобы вы могли найти все эти статьи в дальнейшем, они маркируются специальным тегом: Otp introduction(здесь я сделал ссылку на теги хабра). Как обещано во введении в OTP, мы будем создавать сервер, обслуживающий фейковые банковские аккаунты людей в «Erlybank» (да, я люблю глупые имена).

Сценарий: ErlyBank начинает свою деятельность, и руководителям необходимо встать с правильной ноги, создав масштабируемую систему для управления банковскими аккаунтами их важной базы покупателей. Услышав про мощь Erlang, они наняли нас сделать это! Но чтобы посмотреть, на что мы годны, они сначала хотят увидеть простенький сервер, умеющий создавать и удалять аккаунты, делать депозит и изъятие денег. Заказчики хотят только прототип, а не что-то, что они смогут запустить в производство.

Цель: мы создадим простой сервер и клиент, используя gen_server. Так как это просто прототип, аккаунты будут храниться в памяти и идентифицироваться по имени. Никакой другой информации для создания аккаунта будет не нужно. И конечно же, мы сделаем проверку для операций депозита и изъятия денег.

Примечание: я полагаю, что у вас уже есть начальные знания синтаксиса Erlang. Если нет, то рекомендую прочитать краткое резюме по ресурсам для начинающих, чтобы найти тот ресурс, где вы сможете изучить Erlang.

Если вы готовы, жмите «Читать дальше», чтобы начать! (Если вы уже не читаете всю статью целиком:) )


Что находится в gen_server?


gen_server — это интерфейсный модуль для реализации клиент-серверной архитектуры. Когда вы используете этот OTP модуль, множество вкусностей достается вам «for free», но об этом я расскажу позже. Также, позже в серии, я поговорю о супервизорах и сообщениях об ошибках. А этот модуль практически не претерпит изменений.

Так как gen_server — это интерфейс, вам необходимо реализовать некоторое количество его методов или функций возврата(callbacks англ.):
  • init/1 — Инициализация сервера.
  • handle_call/3 — Обрабатывает call-запрос. Клиент, пославший ему call-запрос, блокируется до тех пор, пока не получит ответ.
  • handle_cast/2 — Обрабатывает cast-запрос. cast-запрос идентичен call-запросу за исключением того, что он асинхронный; клиент продолжает работу во время cast-запроса.
  • handle_info/2 — В своем роде «catch all» метод. Если серверу приходит сообщение, и это не call и не cast, то оно придет сюда. Примером такого сообщения может служить EXIT process сообщение, если ваш сервер соединен(залинкован) с другим процессом.
  • terminate/2 — Вызывается, когда сервер останавливается. Здесь можно сделать любые необходмые перед выходом операции.
  • code_change/3 — Вызывается, когда сервер обновляется в реальном времени. Вы должны поместить заглушку в этот метод. Этот метод будет рассмотрен в деталях в будущих статьях.

Скелет сервера


Я всегда начинаю писать с некоторого обобщенного скелета. Можете посмотреть его здесь.

Примечание: чтобы не загромождать пространство, я не буду вставлять содержимое файлов сюда, я буду стараться связать все, как только появится возможность. Здесь я буду размещать важные отрывки кода.

Как вы видите, модуль называется eb_server. Он имплементирует все callback-методы, которые я указал выше и также добавляет еще один: start_link/0, который будет использоваться для старта сервера. Я вставил порцию этого кода ниже:

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

Здесь вызывается start_link/4 метод модуля gen_server, который стартует сервер и регистрирует процесс с помощью атома, определенного макросом SERVER, который по умолчанию является всего лишь именем модуля. Оставшиеся аргументы — это модуль gen_server, в данном случае он сам, и любые другие аргументы, а в конце опции. Мы оставляем аргументы пустыми(они нам не нужны) и мы не указываем никаких опций. За более подробным описанием этого метода обращайтесь на страницу gen_server manual.

Инициализация Erlybank сервера


Сервер стартуется методом gen_server:start_link, который вызывает метод init нашего сервера. В нем вам следует инициализировать состояние(данные) сервера. Состояние(данные) может быть чем угодно: атомом, списком значений, функцией, чем угодно! Это состояние(данные) передается серверу в каждом callback-методе. В нашем случае мы бы хотели хранить список всех аккаунтов и значений их баланса. Дополнительно, мы бы хотели искать аккаунты по именам. Для этого я собираюсь использовать модуль dict, который сохраняет пары «ключ-значение» в памяти.

Примечание для объекто-ориентированных умов: если вы пришли из ООП, вы можете принять состояние сервера за переменные его экземпляра. В каждом callback методе вам будут доступны эти переменные, и также вы сможете их изменять.

Итак, окончательный вид метода init:

init(_Args) ->
  {ok, dict:new()}.

И правда, это просто! Так, для Erlybank сервера одно из ожидаемых значений, которое возвращает init, — это {ok, State}. Я просто возвращаю ok и пустой ассоциативный массив как состояние(данные). И мы не передаем аргументы в init(которые все равно пустой массив, помните, из start_link), так что я предваряю аргумент символом "_", чтобы указать на это.

Call или Cast? Вот вопрос


Перед тем, как мы разработаем большую часть сервера, я хочу еще раз быстро пройтись по различиям между call и cast методами.

Call — это метод, блокирующий клиент. Это означает, что когда клиент посылает сообщение серверу, он дожидается ответа перед тем, как продолжать дальше. Вы используете call когда вам необходим ответ, например запрос баланса определенного аккаунта.

Cast — это неблокирующий или асинхронный метод. Это означает, что когда клиент посылает сообщение серверу, он продолжает работать дальше, не дожидаясь ответа сервера. Сейчас Erlang гарантирует, что все посланные процессам сообщения дойдут до адресатов, так что, если вы явно не нуждаетесь в ответе сервера, вам следует использовать cast, — это позволит вашему клиенту работать дальше. То есть, вам не нужно делать call только для того, чтобы удостовериться, что ваше сообщение дошло — оставьте это Erlang.

Создание банковского аккаунта


Сначала начало:), Erlybank нужен способ создания новых аккаунтов. Быстро, проверьте себя: если бы вам нужно было создать банковский аккаунт, что бы вы использовали: cast или call? Думайте качественно… Какое значение должно быть возвращено? Если вы выбрали call — вы правы, хотя ничего страшного, если нет. Вы должны быть уверенными, что аккаунт создан успешно, вместо того, чтобы просто полагаться на это. В нашем случае я собираюсь выполнить его через cast, так как проверку ошибок мы сейчас не производим.

Первое, я создам API метод, который будет вызываться извне модуля, чтобы создать аккаунт:

%%--------------------------------------------------------------------
%% Function: create_account(Name) -> ok
%% Description: Creates a bank account for the person with name Name
%%--------------------------------------------------------------------
create_account(Name) ->
  gen_server:cast(?SERVER, {create, Name}).

Он посылает cast-запрос серверу, который мы зарегистрировали как ?SERVER в start_link. Запрос представляет из себя тапл {create, Name}. В случае использования cast немедленно возвращается «ok», что также возвращается нашей функцией.

Сейчас нам надо написать callback-метод для сервера, который будет обрабатывать этот cast:

handle_cast({create, Name}, State) ->
  {noreply, dict:store(Name, 0, State)};
handle_cast(_Msg, State) ->
  {noreply, State}.

Как вы можете видеть, мы только добавили еще одно определение для handle_cast чтобы обработать еще один запрос. Затем мы сохраняем его в массиве со значением 0, отображающим текущий баланс аккаунта. handle_cast возвращает {noreply, State}, где State — это новое состояние(данные) сервера. Итак, в этот раз мы возвращаем новый массив с добавленным аккаунтом.

также, заметьте, что я добавил «catch-all» функцию в конец. Это следует делать не только здесь, но также во всех функциональных языках в целом. Вы можете использовать этот метод только для того, чтобы проглотить сообщение молча:) или вам следует вызвать исключение, если вам надо. В нашем случае я обрабатываю его тихо.

Содержимое файла eb_server.erl на данный момент вы можете посмотреть здесь.

Денежный депозит


Мы обещали нашему клиенту, Erlybank, что мы добавим API для депозита денег и базовую проверку. Так что нам надо написать deposit API метод так, чтобы сервер был обязан проверять аккаунт на существование перед внесением денег на счет. И снова, проверьте себя: cast или call? Ответ прост: call. Мы должны быть уверены, что деньги дошли и уведомить пользователя.

Как и раньше, я пишу сначала API метод:

%%--------------------------------------------------------------------
%% Function: deposit(Name, Amount) -> {ok, Balance} | {error, Reason}
%% Description: Deposits Amount into Name's account. Returns the
%% balance if successful, otherwise returns an error and reason.
%%--------------------------------------------------------------------
deposit(Name, Amount) ->
  gen_server:call(?SERVER, {deposit, Name, Amount}).

Ничего выдающегося: мы просто посылаем сообщение серверу. Вышеупомянутое должно выглядеть знакомым вам, так как пока что это практически то же самое, что cast. Но различия появляются в серверной части:

handle_call({deposit, Name, Amount}, _From, State) ->
  case dict:find(Name, State) of
    {ok, Value} ->
      NewBalance = Value + Amount,
      Response = {ok, NewBalance},
      NewState = dict:store(Name, NewBalance, State),
      {reply, Response, NewState};
    error ->
      {reply, {error, account_does_not_exist}, State}
  end;
handle_call(_Request, _From, State) ->
  Reply = ok,
  {reply, Reply, State}.

Вау! Много нового и непонятного! Определение метода выглядит похожим на handle_cast, исключая новый аргумент From, который мы не используем. Это идентификатор pid вызывающего процесса, так что мы можем послать ему дополнительное сообщение, если понадобится.

Мы обещали Erlybank, что сделаем проверку существования аккаунта, и мы делаем это в первой строчке кода. Мы пытаемся найти значение из массива состояния(данных) эквивалентное пользователю, пытающемуся сделать депозит. Метод find модуля dict возвращает одно из 2-х значений: или {ok, Value}, или error.

В случае, если аккаунт существует, Value равняется текущему балансу аккаунта, — добавляем сумму депозита к нему. Затем мы сохраняем новый баланс аккаунта в массиве и присваиваем его переменной. Я также сохраняю ответ сервера в переменной, чтио выглядит просто как комментарий к deposit API, говорящий: всё {ok, Balance}. Потом, возвращая {reply, Reply, State}, сервер посылает обратно Reply и сохраняет новое состояние(данные).

С другой стороны, если аккаунт не существует, мы не изменяем состояние(данные) вовсе, а в ответ посылаем тапл {error, account_does_not_exist}, который опять же следует спецификации в комментариях deposit API.

Снова, здесь обновленная версия eb_server.erl.

Удаление аккаунта и снятие денег


Сейчас я собираюсь оставить как упражнение читателю написание API для удаления аккаунта и снятия денег с аккаунта. У вас есть все необходимые знания для того, чтобы сделать это. Если вам нужна помощь с модулем dict, обращайтесь к dict API reference. При снятии денег со счета проверяйте аккаунт как на существование, так и на наличие необходимой для снятия суммы денег. Вам не нужно обрабатывать отрицательные значения.

Когда вы закончите, или если вы не закончите(надеюсь, нет!), можете найти ответы здесь.

Заключительные примечания


В этой статье я показал основы gen_server и как создавать клиент-серверное общение. Я не рассказывал про все возможности gen_server, которые включают такие вещи, как таймауты для сообщений и остановка сервера, но я поведал про солидную порцию этого интерфейса.

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

Также, я знаю, что не касался the метода code_change/3, который по каким-то причинам является самым вкусным для людей. Не волнуйтесь, у меня уже есть наброски для статьи(ближе к концу серии), посвященной проведению апгрейдов на работающей системе(горячая замена кода), и вот там этот метод будет играть одну из главных ролей.

Следующая статья будет через несклько дней. Она будет посвящена gen_fsm. Так что, если эта статья пощекотала вам мозг, «почувствуйте свободу прыгнуть в мануал»:) и действуйте сами. Может быть, вам удастся угадать продолжение истории ErlyBank, которое я буду делать с gen_fsm.;)
Tags:
Hubs:
Total votes 13: ↑13 and ↓0+13
Comments28

Articles