Что такое OTP
OTP — это набор проверенных временем паттернов для построения надёжных конкурентных систем. Вместо ручных spawn и receive вы используете готовые «поведения».
OTP — причина, по которой системы на BEAM работают годами без перезапуска. Это не библиотека «на потом», а сердце экосистемы.
В прошлом разделе мы руками писали цикл receive, хранили состояние в аргументах, ловили падения через monitor. Всё это — повторяющаяся рутина с тонкими граблями. OTP инкапсулирует её в behaviours (поведения): GenServer (сервер с состоянием), Supervisor (надзор и перезапуск), Application (запуск дерева процессов), и другие.
Behaviour — это контракт: фреймворк берёт на себя цикл сообщений, а вы реализуете лишь callback-функции «что делать на такое сообщение».
# Вместо ручного loop/receive — реализуем колбэки поведения
defmodule Counter do
use GenServer # подключаем поведение
def init(initial), do: {:ok, initial}
def handle_call(:get, _from, count) do
{:reply, count, count}
end
end
Само OTP-приложение описывается деревом процессов, которое запускается при старте:
# mix создаёт OTP-приложение с supervision tree
$ mix new my_app --sup
Как работает под капотом (BEAM)
Behaviour — это разделение труда между «общим модулем» (например, :gen_server) и вашим callback-модулем. Общий модуль реализует скучную, легко ошибиться часть: цикл receive, обработку системных сообщений, корректное завершение, интеграцию с супервизором. Ваш модуль реализует только доменные колбэки: init, handle_call, handle_cast. Когда приходит сообщение, generic-цикл матчит его и вызывает нужный ваш колбэк. Так десятилетия отлаженного кода переиспользуются, а вы пишете лишь бизнес-логику.
Behaviour = generic-часть (OTP) + ваши колбэки
[:gen_server цикл receive] --вызывает--> handle_call/cast/info
(отлажено годами) (ваша логика)
Та же идея на Python ▶
Идея «фреймворк ведёт цикл, вы пишете колбэки» — это шаблонный метод/инверсия управления.
# Generic-часть (как OTP behaviour) ведёт цикл и зовёт ваши колбэки
class GenServerLike:
def __init__(self, state):
self.state = state
def run(self, messages): # generic цикл (OTP делает это за вас)
replies = []
for msg in messages:
reply, self.state = self.handle(msg, self.state)
if reply is not None:
replies.append(reply)
return replies
def handle(self, msg, state): # ВЫ переопределяете только это
raise NotImplementedError
class Counter(GenServerLike): # ваш callback-модуль
def handle(self, msg, state):
match msg:
case "inc": return (None, state + 1)
case "get": return (state, state)
c = Counter(0)
print(c.run(["inc", "inc", "get", "inc", "get"])) # [2, 3]
Частые ошибки
- Писать процессы руками «для контроля». Ручной receive почти всегда хуже GenServer: вы упустите системные сообщения и завершение.
- Считать OTP сложным «на потом». Наоборот — он упрощает; без него код надёжных систем разрастается.
- Забыть
--supприmix new, когда проекту нужно дерево процессов.
Best practices
- Для процессов с состоянием берите GenServer, а не голый spawn/receive.
- Организуйте систему деревом супервизоров, запускаемым из Application.
- Думайте о приложении как о наборе процессов под надзором, а не как о последовательном скрипте.
Итог. OTP превращает сырые процессы в надёжные строительные блоки через behaviours. Главный из них — GenServer; разберём его подробно в следующем уроке.
Палитра поведений OTP
GenServer — самое известное, но не единственное поведение. Supervisor отвечает за надзор и перезапуск, Application — за запуск дерева процессов при старте узла, Task и Agent — за более лёгкие сценарии (разовая параллельная работа и простое хранилище состояния соответственно). Есть и более специальные, вроде GenStage для конвейеров данных с обратным давлением. Все они следуют одному принципу: фреймворк ведёт скучную общую часть, вы пишете доменные колбэки.
Эта зрелость — главная причина, по которой системы на BEAM славятся аптаймом. Десятилетия эксплуатации в телекоме вычистили из generic-части OTP тонкие баги, которые вы неизбежно бы наделали, реализуя цикл процесса вручную: корректную обработку системных сообщений, упорядоченное завершение, hot code upgrade, интеграцию с инструментами наблюдения. Используя поведение, вы наследуете всю эту надёжность бесплатно. Поэтому в Elixir-сообществе «напиши свой receive-цикл вместо GenServer» считается почти всегда ошибкой — и теперь вы понимаете, почему.