GenServer (загальний сервер) - це поведінка, яка абстрагує типові взаємодії між клієнтом і сервером у процесах Elixir.
Пригадаймо цикл отримання повідомлень, про який ми дізналися, коли вивчали процеси? Поведінка GenServer надає абстракції для реалізації таких циклів і для обміну повідомленнями з процесом, який виконує такий цикл. Вона спрощує збереження стану і виконання асинхронного коду.
Зауважте, що назва GenServer багатозначна. Нею також позначають модуль, який використовує поведінку GenServer, а також процес, запущений з модуля, який використовує поведінку GenServer.
Поведінка GenServer визначає один обовʼязковий колбек, init/1, і кілька цікавих необовʼязкових колбеків: handle_call/3, handle_cast/2 і handle_info/3. Клієнти, які використовують GenServer, не повинні викликати ці колбеки напряму. Натомість модуль GenServer надає функції, які клієнти можуть використовувати для звʼязку з процесом GenServer.
Часто один модуль визначає і клієнтський API, набір функцій, які інші частини застосунку Elixir можуть викликати, щоб взаємодіяти з цим процесом GenServer, і реалізації серверних колбеків, які містять логіку цього GenServer.
Спершу розгляньмо простий приклад GenServer, а потім дізнаймося, що означає кожен колбек.
Це приклад сервера, який відповідає на навʼязливі запитання дратівливих пасажирів під час довгої поїздки, а точніше на запитання: «ми вже на місці?». Він рахує, скільки разів це запитання вже поставили, і щоразу відповідає дедалі роздратованіше.
defmodule AnnoyingPassengerAutoresponder do
use GenServer
# Client API
def start_link(init_arg) do
GenServer.start_link(__MODULE__, init_arg)
end
def are_we_there_yet?(pid) do
GenServer.call(pid, :are_we_there_yet?)
end
# Server callbacks
@impl GenServer
def init(_init_arg) do
# the initial count of questions asked is always 0
state = 0
{:ok, state}
end
@impl GenServer
def handle_call(:are_we_there_yet?, _from, state) do
reply =
cond do
state <= 3 -> "No."
state <= 10 -> "I told you #{state} times already. No."
true -> "..."
end
# increase the count of questions asked
new_state = state + 1
# reply to the caller
{:reply, reply, new_state}
end
end
init/1Сервер можна запустити, викликавши GenServer.start/3 або GenServer.start_link/3. Про різницю між цими функціями ми дізналися в концепції звʼязків.
Ці дві функції:
GenServer.init_arg. Як підказує назва, цей аргумент передається в колбек init/1.Запуск сервера викликом GenServer.start/3 або GenServer.start_link/3 викличе колбек init/1 у блокуючий спосіб. Повернене значення init/1 визначає, чи сервер зможе успішно запуститися.
Колбек init/1 зазвичай повертає одне з таких значень:
{:ok, state}. Сервер почне свій цикл отримання повідомлень, використовуючи state як початковий стан. state може бути будь-якого типу.{:stop, reason}. reason може бути будь-якого типу. Сервер не почне свій цикл отримання повідомлень. Процес завершиться із зазначеної причини.Існують також складніші можливості, які ми зараз не розглядаємо.
Якщо цикл отримання повідомлень сервера запускається, функції GenServer.start/3 і GenServer.start_link/3 повертають кортеж {:ok, pid}. Інакше вони повертають {:error, reason}
handle_call/3Повідомлення, яке потребує відповіді, можна надіслати процесу сервера за допомогою GenServer.call/2. Ця функція приймає pid запущеного процесу сервера як перший аргумент і повідомлення як другий. Повідомлення може бути будь-якого типу.
Колбек handle_call/3 відповідає за обробку синхронних повідомлень і відповідь на них. Він отримує три аргументи:
message - значення, передане як другий аргумент у GenServer.call/2.from - pid процесу, який викликає GenServer.call/2. Найчастіше цей аргумент можна проігнорувати.state - поточний стан сервера. Пригадаймо, що його початкове значення було встановлено в колбеку init/1.Колбек handle_call/3 зазвичай повертає кортеж із трьох елементів {:reply, reply, state}. Це означає, що другий елемент кортежу, reply, який може бути будь-якого типу, буде надіслано назад тому, хто викликав. Третій елемент кортежу, state, стає новим станом сервера після обробки цього повідомлення.
Існують також складніші можливості, які ми зараз не розглядаємо.
Щоб запамʼятати за назвою, що робить цей колбек, уявімо, що ми «телефонуємо» комусь.
Якщо ця людина доступна, ми одразу (синхронно) отримаємо відповідь.
handle_cast/2Повідомлення, яке не потребує відповіді, можна надіслати процесу сервера за допомогою GenServer.cast/2. Його аргументи такі самі, як у GenServer.call/2.
Колбек handle_cast/2 відповідає за обробку таких повідомлень. Він отримує два аргументи, message і state, ті самі, що й у колбеку handle_call/3 (крім from).
Колбек handle_cast/2 зазвичай повертає кортеж із двох елементів {:noreply, state}.
Існують також складніші можливості, які ми зараз не розглядаємо.
Щоб запамʼятати за назвою, що робить цей колбек, пригадаймо, що англійське «to cast» означає також «кидати».
Якщо ми кидаємо пляшку з повідомленням у море, то не очікуємо отримати відповідь одразу, а можливо, й ніколи.
call чи cast?Майже завжди використовуймо call, навіть якщо наш клієнтський код не потребує відповіді від сервера.
Використання call чекає на відповідь, що слугує механізмом зворотного тиску (щоб клієнти не надсилали надто багато повідомлень одразу). Отримання відповіді від сервера також є єдиним способом переконатися, що сервер отримав і обробив повідомлення клієнта.
handle_info/2Повідомлення можуть також потрапляти до поштової скриньки сервера не тільки через виклик GenServer.call/2 чи GenServer.cast/2, наприклад через звичайний виклик функції send/2.
Для обробки таких повідомлень використовуймо колбек handle_info/2. Цей колбек працює точно так само, як handle_cast/2.
Поведінка GenServer надає універсальну реалізацію handle_info/2, яка записує в журнал помилки про неочікувані повідомлення. Якщо ми перевизначаємо цю типову реалізацію, потрібно завжди додавати власну універсальну реалізацію. Якщо ми забудемо, сервер аварійно завершиться, отримавши неочікуване повідомлення.
Повернене значення кожного з чотирьох описаних вище колбеків можна розширити ще одним елементом кортежу, тайм-аутом. Наприклад, замість повернення {:ok, state} з init/1, повернімо {:ok, state, timeout}.
Тайм-аут дає змогу виявити відсутність повідомлень у поштовій скриньці протягом певного проміжку часу. Якщо сервер повертає тайм-аут з одного зі своїх колбеків і вказана кількість мілісекунд минула, а повідомлення так і не надійшло, викликається handle_info/2 з :timeout як першим аргументом.