Phoenix (Elixir)SeniorSystem design
Что такое Phoenix Channels и как они обеспечивают коммуникацию в реальном времени?
Phoenix Channels — это абстракция реального времени: каждое подключение клиента — отдельный BEAM-процесс, маршрутизируемый по теме (topic) через Socket, с двусторонней передачей событий через PubSub.
Что такое Phoenix Channels
Phoenix Channels — это абстракция для двусторонней коммуникации в реальном времени поверх транспортного протокола (WebSocket, Long Poll). Каждый канал — это отдельный Elixir-процесс (GenServer), изолированный на уровне BEAM. Клиент подключается к Socket, а затем вступает в конкретный Channel по строке темы, например "room:42" или "user:#{id}".
Ключевые сущности
- Socket — точка входа. Определяет аутентификацию и маршрутизацию на каналы.
- Channel — модуль, реализующий
use Phoenix.Channel. Содержит колбэкиjoin/3,handle_in/3,handle_out/3,handle_info/2. - Topic — строка вида
"entity:id"; первая часть сопоставляется вchannel/2сокета. - PubSub — шина событий, по которой серверные процессы рассылают сообщения всем подписчикам темы.
Пример: чат-канал
defmodule MyAppWeb.RoomChannel do
use Phoenix.Channel
def join("room:" <> _room_id, _params, socket) do
{:ok, socket}
end
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast!(socket, "new_msg", %{body: body})
{:noreply, socket}
end
end
Регистрация в сокете:
defmodule MyAppWeb.UserSocket do
use Phoenix.Socket
channel "room:*", MyAppWeb.RoomChannel
def connect(%{"token" => token}, socket, _connect_info) do
case Phoenix.Token.verify(socket, "user", token, max_age: 86400) do
{:ok, user_id} -> {:ok, assign(socket, :user_id, user_id)}
{:error, _} -> :error
end
end
def id(socket), do: "user_socket:#{socket.assigns.user_id}"
end
JavaScript-клиент:
import { Socket } from "phoenix";
const socket = new Socket("/socket", { params: { token: userToken } });
socket.connect();
const channel = socket.channel("room:42", {});
channel.on("new_msg", ({ body }) => console.log(body));
channel.push("new_msg", { body: "Hello" });
channel.join()
.receive("ok", () => console.log("joined"))
.receive("error", (e) => console.error(e));
Жизненный цикл сообщения
- Клиент отправляет событие — оно десериализуется в процессе сокета и диспетчеризуется в нужный канал.
handle_inобрабатывает событие и может ответить черезreply/2,push/3илиbroadcast!/3.broadcast!публикует в Phoenix.PubSub; все процессы-каналы, подписанные на тему, получают событие и пушат его своим клиентам.- Каждый процесс канала изолирован: краш одного клиента не затрагивает других.
Подводные камни
- Блокирующий код в handle_in. Вызов тяжёлой DB-операции синхронно блокирует процесс канала. Делегируйте в Task или отдельный GenServer.
- Неограниченный topic-namespace. Паттерн
"room:*"разрешает любую строку; без проверки вjoin/3клиент может подписаться на чужую комнату. - broadcast! vs broadcast.
broadcast!бросает исключение при сбое PubSub. В критичном коде используйтеbroadcast/3и обрабатывайте{:error, reason}. - Нет персистентности сообщений. Channels — ephemeral. Клиент, отключившийся на секунду, пропускает все события; нужен отдельный механизм (read cursor, replay endpoint).
- Большой payload в assigns. Socket assigns хранятся в памяти процесса на весь сеанс. Не кладите туда большие структуры данных.
- handle_out перехватывает broadcast глобально. Если переопределить
handle_outбез явногоpush/3, сообщение не дойдёт до клиента — легко пропустить при рефакторинге. - Неправильное id/1 в сокете.
id/1используется для принудительного отключения всех сессий пользователя черезMyAppWeb.Endpoint.broadcast("user_socket:#{id}", "disconnect", %{}). Если вернуть nil, функция отключения не сработает.
Common mistakes
- Сводить channels к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: Phoenix 1.8 работает поверх Plug, Endpoint, Router, Controllers/LiveViews, PubSub и OTP supervision.
- Не отделять validation, authorization, transaction boundary и business logic.
- Не обсуждать idempotency, retries, shutdown и observability.
What the interviewer is testing
- Объясняет channels через конкретную точку lifecycle в Phoenix (Elixir).
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.
- Связывает решение с метриками, backpressure, retry policy и graceful shutdown.