Отложенные API-вызовы к Twilio с помощью Rails, Active Job и Sidekiq

Автор: | 01.08.2026

Производительность — ключевой аспект веб-приложений. Быстрые сайты обеспечивают лучший пользовательский опыт, повышают конверсию и удержание пользователей. Быстрый отклик приложения также снижает нагрузку на серверы, обслуживающие множество пользователей. В Rails существует множество способов улучшить производительность веб-приложения, и сегодня мы рассмотрим один из них.

Выполнение длительных блокирующих задач в процессе обработки запроса — одна из основных причин замедления работы любого веб-приложения. Речь идет о таких действиях, как отправка электронных писем, генерация PDF- или CSV-файлов или выполнение HTTP-запросов к сторонним API. Все эти операции занимают относительно много времени по сравнению с другими действиями, выполняемыми в рамках запроса. Если вы используете Twilio в своем приложении, последний пункт может вас заинтересовать.

Лучшее решение для таких длительных задач — перенести их выполнение из основного запроса в фоновый режим. Это позволит серверу приложения быстро отвечать на запросы и выполнять задачи без ущерба для производительности сайта и без блокировки процессов веб-сервера. Начиная с версии 4.2, Rails включает Active Job — библиотеку, которая упрощает откладывание выполнения длительных задач и их обработку в фоновой очереди. В этой статье мы рассмотрим, как использовать Active Job для ускорения работы приложения.

Чтобы продемонстрировать, как можно ускорить время отклика приложения, перенося задачи в фоновую очередь, нам понадобится тестовое приложение. Вместо того чтобы создавать его с нуля, мы воспользуемся одним из примеров из учебных материалов Twilio. Давайте рассмотрим приложение «Click to Call» — простую форму на сайте, которая принимает номер телефона пользователя и перезванивает ему. Вы можете создать приложение по инструкции или скачать его с GitHub.

Для выполнения этой задачи вам понадобятся:

— Аккаунт Twilio (зарегистрируйтесь, если у вас его еще нет)

— Номер Twilio, с которого можно совершать звонки

— Способ пробросить публичный URL на локальный хост (я рекомендую ngrok)

— Установленные Ruby и bundler

— Redis (ознакомьтесь с руководством по быстрому старту Redis, чтобы запустить его)

Чтобы запустить приложение, возьмите учетные данные Twilio из панели управления аккаунтом и номер телефона, затем:

— Клонируйте репозиторий: $ git clone https://github.com/TwilioDevEd/clicktocall-rails

— Перейдите в директорию приложения: $ cd clicktocall-rails

— Установите гемы: $ bundle install

— Задайте переменные окружения:

— Выполните миграции базы данных (пока миграций нет, но это поддерживает работоспособность приложения): $ bundle exec rake db:migrate

— Запустите тесты: $ bundle exec rake test

— Запустите сервер: $ bundle exec rails server

— В отдельной вкладке консоли настройте туннель: $ ngrok http 3000

Теперь мы готовы протестировать приложение. Откройте его в браузере, используя публичный URL из туннеля. Вы увидите форму, запрашивающую ваш номер телефона. Введите номер и отправьте форму — вы получите сообщение об успехе и входящий звонок.

Все выглядит отлично, не так ли? Возможно, вы задаетесь вопросом: «В чем проблема Фила?» Давайте посмотрим логи:

Загрузка главной страницы заняла всего 38 мс, а начало звонка — чуть больше секунды. Хотя это приемлемо на начальном этапе или при небольшом трафике, со временем это станет проблемой.

Остановите сервер с помощью Ctrl+C и давайте исправим ситуацию.

Как я уже упоминал во введении, решение для запросов с длительными блокирующими задачами — перенести их выполнение в фоновый режим. Сейчас мы сделаем это с помощью Active Job. Сначала посмотрим на действие, с которым работаем:

Действие получает номер телефона из параметров и создает объект ContactTwilio::REST::Client, который затем используется для совершения звонка с вашего номера Twilio на введенный номер, указывая на connect_url (см. действие connect для дальнейших действий).

Я выделил API-вызов в коде выше. Это наша длительная задача, которую нужно перенести в фоновую очередь. Давайте создадим задачу для ее обработки. В командной строке введите:

Это создаст два новых файла: app/jobs/make_call_job.rb и test/jobs/make_call_job_test.rb. Чтобы убедиться, что задача работает правильно, мы можем скопировать часть тестов из теста контроллера в тест задачи.

Если мы запустим тесты сейчас, то увидим ошибку.

Давайте исправим это. К счастью, весь необходимый код уже есть в контроллере, как я показал ранее. Нам нужно обновить MakeCallJob. Классы Active Job должны определять только две вещи: имя очереди, которое мы можем оставить как :default, и метод perform. Мы можем перенести API-запрос из контроллера в метод perform нашей задачи.

Запустите тесты снова — они пройдут. Отлично, мы на полпути. Теперь нужно изменить контроллер. Вместо того чтобы совершать звонок через API, действие должно добавлять задачу в очередь с нужными аргументами. Мы можем сделать это с помощью утверждения assert_enqueued_with. Откройте test/controllers/twilio_controller_test.rb и измените следующий тест:

Чтобы использовать это продвинутое утверждение assert_enqueued_with, нужно подключить хелперы тестирования Active Job. В начало теста добавьте строку:

Запустите тесты снова — вы увидите еще одну ошибку.

Нам нужно обновить контроллер, чтобы он ставил задачу в очередь вместо совершения звонка. Классы Active Job определяют метод экземпляра perform, но для добавления задач в очередь используется метод класса perform_later. По умолчанию perform_later ставит задачу в очередь для выполнения, как только освободится воркер. Также можно запланировать выполнение задачи на определенное время в будущем с помощью метода set, за которым следует perform_later, например: MakeCallJob.set(wait_until: 3.days.from_now).perform_later.

Так как мы хотим, чтобы задача выполнялась как можно быстрее, обновим контроллер, чтобы он вызывал MakeCallJob.perform_later с двумя аргументами, которые мы определили для задачи: номер телефона и URL, который Twilio должен запросить при ответе на звонок. Откройте app/controllers/twilio_controller.rb и замените строки, где создается и используется Twilio::REST::Client, на вызов класса задачи.

Запустите тесты снова — вы увидите успех. Теперь давайте проверим это на практике. Запустите сервер снова, введите номер телефона и дождитесь звонка.

Звонок поступает корректно. Но в логах все еще видно, что обработка занимает больше секунды. Что происходит?

Active Job хорош тем, что предоставляет простой интерфейс для постановки задач в очередь. Однако «из коробки» Active Job предоставляет только реализацию по умолчанию, которая сразу же выполняет задачи в основном потоке. Чтобы перенести работу в фоновый режим, нам нужно подключить другой бэкенд. Active Job поддерживает несколько популярных очередей задач для Ruby, включая Sidekiq, Resque и Delayed Job. Полный список адаптеров и их возможностей можно найти в документации Active Job.

Мне нравится Sidekiq: он использует Redis для хранения задач (Delayed Job хорошо интегрируется с Active Record, но это может быть медленно) и воркеров на основе потоков (Resque использует воркеров на основе процессов, что может занимать больше памяти). Мы завершим этот пример с использованием Sidekiq, но я рекомендую изучить другие варианты и выбрать тот, который подходит именно для вашего приложения.

Откройте Gemfile и добавьте Sidekiq как зависимость. Добавьте строку под gem ‘twilio-ruby’:

Запустите $ bundle install для установки Sidekiq, затем откройте config/application.rb, чтобы задать бэкенд Active Job:

Теперь все, что нам нужно сделать, — это перезапустить приложение, запустить процесс Sidekiq, который загрузит несколько воркеров для обработки наших задач, и наблюдать за ускорением работы приложения. Убедитесь, что Redis запущен на этом этапе — Sidekiq будет его использовать. Если вы установили Redis, запустите его в отдельной вкладке консоли с помощью команды:

Чтобы запустить Sidekiq, откройте новую вкладку консоли, перейдите в директорию приложения, задайте те же переменные окружения, что и для приложения, используя ваши учетные данные и номер Twilio, и запустите Sidekiq:

Замечательный ASCII-арт, не правда ли? Загрузите приложение снова, введите номер телефона и дождитесь звонка. Вы не заметите большой разницы в использовании приложения, но посмотрите на логи приложения.

Да, наше действие звонка теперь выполняется всего за ~50 мс. Используя Active Job и Sidekiq, мы сняли всю нагрузку с процессов веб-приложения и перенесли ее на фоновый процесс.

При переносе таких задач из веб-процесса в фоновый режим нужно учитывать несколько моментов. Как мы уже обсуждали ранее, выбор бэкенда очереди важен. Например, если вы еще не используете Redis, добавление Resque или Sidekiq означает добавление новой зависимости для вашего проекта. Если вам нужно обрабатывать много задач, использование Active Record в Delayed Job также может быть нецелесообразным.

Также стоит подумать о пользовательском интерфейсе. В этом примере, если создание звонка завершится неудачей, пользователь не увидит ошибку — он просто не получит звонок. Возможно, стоит отслеживать прогресс выполнения задачи и позже сообщать пользователям об ошибках.

В этой статье мы взяли действие контроллера с длительной задачей и ускорили его отклик, перенеся работу в фоновый режим. Мы увидели, как Active Job упрощает этот процесс в Rails-приложениях, и выбрали Sidekiq в качестве системы очередей. Ознакомьтесь с финальным кодом на GitHub.

Теперь, когда вы попробовали Sidekiq, возможно, стоит рассмотреть, что могут предложить другие бэкенды очередей. Resque и Delayed Job — другие популярные варианты, но также обратите внимание на Sneakers, который использует RabbitMQ для хранения задач, или Que, который полагается на PostgreSQL.

Если вы хотите сделать то же самое для отправки SMS-сообщений, ознакомьтесь с гемом textris, который оборачивает SMS в интерфейс, похожий на Action Mailer, включая простую интеграцию с Active Job.

Active Job — одна из тех функций Rails, появления которой долго ждали, но теперь она значительно упрощает жизнь. Если у вас есть вопросы по использованию фоновых задач или интересные способы применения очередей, оставляйте их в комментариях или напишите мне в Twitter @philnash.