Что такое 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» считается почти всегда ошибкой — и теперь вы понимаете, почему.

Проверьте себя
1. Что такое behaviour (поведение) в OTP?
AТип данных
BКонтракт, где generic-часть OTP ведёт цикл сообщений, а вы реализуете лишь callback-функции с бизнес-логикой
CСпособ объявить переменную
DСтиль форматирования кода
2. Почему обычно не пишут процессы вручную через spawn/receive?
AЭто запрещено компилятором
BGenServer и другие behaviours переиспользуют годами отлаженный код (системные сообщения, завершение, надзор), оставляя вам только бизнес-логику
CРучные процессы не компилируются
Dspawn недоступен