Ruby on RailsSeniorSystem design

Что такое ActionCable в Rails и для чего он используется?

ActionCable — встроенный WebSocket-фреймворк Rails, работающий поверх Rack hijack. Позволяет создавать «каналы» для двусторонней real-time коммуникации с браузером через pub/sub на Redis.

Что такое ActionCable

ActionCable интегрирует WebSocket в Rails-приложение как первоклассный гражданин. Он использует Rack hijack API для удержания TCP-соединения открытым и обёртывает его абстракцией «канал» (Channel). Внутри используется EventMachine или async-адаптер для обработки тысяч одновременных соединений в рамках одного процесса.

Основные компоненты

  • Connection — объект соединения, аутентифицирует пользователя при handshake.
  • Channel — серверный класс, наследует ApplicationCable::Channel, определяет методы subscribed, unsubscribed и action-методы.
  • Subscription — клиентская сторона, JavaScript-объект, подписывающийся на канал.
  • Adapter — pub/sub бэкенд: Redis (production), Async (development), PostgreSQL (альтернатива).

Пример: чат-канал

# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    room = Room.find(params[:room_id])
    reject unless current_user.can_access?(room)
    stream_from "chat_room_#{room.id}"
  end

  def speak(data)
    message = Message.create!(
      room_id: params[:room_id],
      user: current_user,
      body: data["body"]
    )
    ActionCable.server.broadcast(
      "chat_room_#{message.room_id}",
      { user: current_user.name, body: message.body }
    )
  end
end
# app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private

    def find_verified_user
      if (token = request.params[:token])
        User.find_by!(ws_token: token)
      else
        reject_unauthorized_connection
      end
    end
  end
end
// JavaScript-клиент (Stimulus / Vanilla)
import consumer from "./consumer";

const subscription = consumer.subscriptions.create(
  { channel: "ChatChannel", room_id: 42 },
  {
    received(data) {
      console.log(data.user, data.body);
    },
    speak(body) {
      this.perform("speak", { body });
    },
  }
);

Конфигурация Redis-адаптера

# config/cable.yml
production:
  adapter: redis
  url: <%= ENV["REDIS_URL"] %>
  channel_prefix: myapp_production

Broadcast из фоновой задачи

Из любого места приложения (модель, Job, сервис) можно вызвать:

ActionCable.server.broadcast(
  "notifications_#{user.id}",
  { type: "order_shipped", order_id: order.id }
)

Масштабирование

ActionCable работает в async-режиме внутри Puma. При нескольких процессах/серверах все они подписываются на один Redis-канал, поэтому broadcast доходит до нужного клиента независимо от того, к какому поду он подключён. Для Kubernetes используйте channel_prefix чтобы изолировать окружения.

Подводные камни

  • Каждое открытое WebSocket-соединение удерживает поток или файловый дескриптор — при 10 000 соединений нужен тюнинг ulimit и config.action_cable.worker_pool_size.
  • ActionCable не работает нормально за nginx без директив proxy_http_version 1.1 и proxy_set_header Upgrade $http_upgrade.
  • Вызов тяжёлых операций напрямую внутри action-метода канала блокирует весь пул потоков — выносите в ActiveJob.
  • Redis-адаптер сохраняет подписки только до перезапуска сервера: reconnect клиента нужно обрабатывать явно через consumer.subscriptions.reload().
  • CSRF-защита на WS не работает стандартным способом — аутентифицируйте через request.session или подписанный токен, не через куки напрямую.
  • При деплое без sticky sessions часть клиентов будет переподключаться к другому поду и снова проходить handshake — добавьте exponential backoff на клиенте.
  • Тестирование через ActionCable::Channel::TestCase требует явного вызова subscribe и проверки transmissions — легко пропустить при написании specs.

Common mistakes

  • Сводить action cable к названию метода без lifecycle и failure path.
  • Игнорировать модель runtime: Rails 8.1 строит приложение вокруг Rack, routing, controllers, Active Record, views и conventions over configuration.
  • Не отделять validation, authorization, transaction boundary и business logic.
  • Не обсуждать idempotency, retries, shutdown и observability.

What the interviewer is testing

  • Объясняет action cable через конкретную точку lifecycle в Ruby on Rails.
  • Приводит корректный минимальный пример без вымышленных методов или callbacks.
  • Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.
  • Связывает решение с метриками, backpressure, retry policy и graceful shutdown.

Sources

Related topics