Grape: не рельсами едиными, ч. 2

в 10:23, , рубрики: api, ruby, Веб-разработка

Grape: не рельсами едиными, ч. 2 - 1В первом своем посте про Grape я быстренько проскакал по основным его возможностям, за что заслуженно был засыпан упреками в излишней сжатости подачи материала. По результатам публикации прошлого поста я пообещал практический пример применения фреймворка, а также сравнительные бенчмарки.

Сегодня мы приступим к созданию примера — разработаем API, которое умеет:
— регистрировать пользователя
— активировать его по email
— обновлять данные авторизованного пользователя
— возвращать профиль авторизованного пользователя

TL;DR

gem install grape-gen

Если честно, я немного слукавил — писать мы ничего не будем.
После того поста я некоторое время думал — как же мне поступить? Просто так тратить время на написание сферического примера в вакууме мне не очень хотелось — хотелось, как говорится, «и рыбку съесть и косточкой не подавиться».
Поэтому я потратил немного времени и собрал гем-генератор полнофункционального приложения API на grape, назвав его незамысловато — grape-gen.

«Скелет» приложения, батарейки в комплекте

Не буду томить вас долгими рассказами о том, что нужно современному веб-приложению, кроме самого фреймворка.
Генератор «из коробки» предлагает:
— ORM (пока только mongoid)
— авторизацию (Warden + CanCanCan)
— фоновые задачи (Sidekiq)
— сообщения в реальном времени (Faye)
— интеграцию с ElasticSearch (Tire)
— загрузку файлов (Carrierwave)
— отправку email через Mandrill (MandrillMailer)
— настроенное тестовое окружение с guard+spork и гемом для документирования API с помощью тестов.

Все это склеено вместе и готово к работе.
Настоятельно рекомендуется redis, нужен как минимум для faye и sidekiq.

После gem install grape-gen в шелле станет доступна одноименная команда. Для генерации приложения выполняем:

$ grape-gen app your_app_name

В генераторе предусмотрена возможность отключать те или иные компоненты, вот хелп к команде

$ grape-gen help app

Usage:
  grape-gen app APP_NAME

Options:
  [--path=PATH]                                            
  [--orm=ORM]                                              
                                                           # Default: mongoid
  [--batteries=one two three]                              # Batteries to include
                                                           # Default: ["sidekiq", "carrierwave", "mandrill", "es", "faye"]
  [--use-grape-rackbuilder], [--no-use-grape-rackbuilder]  
                                                           # Default: true

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

Про EventMachine

В комментариях к первому посту пользователь stalkerg выразил мысль, что «без асинхронного программирования такой фреймворк имеет мало смысла».
Я считаю, что в этом есть доля правды, поэтому «из коробки» приложение предназначено к запуску под EventMachine-based сервером — Thin или Goliath.
Это означает, что все используемые в приложении «батарейки» при загрузке препарируются на предмет замены блокирующего IO на асинхронные аналоги из EventMachine, а для Carrierwave обработка изображений выносится в thread-pool через EventMachine.defer (да, у нас GIL, но даже в этом случае за одинаковое количество времени мы обрабатываем в два раза больше изображений при этом давая event-loop «вздыхать» в два раза чаще — я проверял тестами).
Ну и em-synchrony, конечно. Каждый запрос у нас выполняется в собственном fiber через Rack::FiberPool, так что никакого callback-hell.

API, предоставляемое приложением

POST /api/auth/register 
  email
  password
  display_name
POST /api/auth/approve_email
  email
  email_approvement_code
PUT /api/profile 
  display_name
  avatar
  remove_avatar
GET /api/profile

API у нас отдает JSON, который генерируется с помощью JBuilder и сериализуется через MultiJson+oj.

Конфигурирование приложения

Подключения к redis, elasticsearch, ключи сторонних API настраиваются в файле config/application.yml
Подключения к базе данных настраивается в config/database.yml
Настройки логгирования в config/logging.yml
Настройки sidekiq в config/sidekiq.yml

Пробный запуск

После того, как мы удостоверились в правильности наших конфигов, настало время запустить наше новоиспеченное приложение:

$ RACK_ENV=production thin start -p 9292 # Запуск сервера API
$ thin start -p 9393 -e production -R faye.ru # Запуск сервера faye
$ sidekiq -C config/sidekiq.yml -r ./config/boot_sidekiq.rb -e production # Запуск демона Sidekiq

После того, как процессы успешно стартанут, по адресу http://localhost:9292/faye будет доступна страница с примитивным faye-клиентом, подписанным на каналы /user/registered и /time
Сообщения в канал /time отправляются Sidekiq-задачей, запланированной запускаться раз в 5 секунд.
Таким образом каждые 5 секунд на страницу будет добавляться строка с временем сервера.

После регистрации пользователя и подтверждения его email в канал /user/registered добавляется сообщение с его display_name, получив которое браузер добавит строку с предложением поприветствовать нового пользователя.

Тестовое окружение

Тестовое окружение базируется на гемах RSpec 3, rspec_api_documentation. В комплекте полюбившиеся многим FactoryGirl, DatabaseCleaner и Faker

Запускается все это под Guard+Spork, плюс в тестовом окружении, как и в development, используется перезагрузка кода, что позволяет прогонять тесты достаточно быстро.

Отдельно стоит сказать про гем rspec_api_documentation — он позволяет совместить процесс написания тестов и формирования документации API.
До этого я использовал Swagger, но к сожалению он в большей степени подходит для каноничных REST API. Если у вас API больше в стиле JSON RPC, то вам будет непросто вместить свое API в описательную структуру Swagger, при этом документирование структуры ответов API доступно только для grape-entity.Вышеупомянутый гем с помощью своего DSL поверх RSpec позволяет документировать API с помощью примеров: вы описываете тестовый пример (например, валидную регистрацию пользователя), при запуске этого примера он запоминает посланный на сервер запрос, полученный ответ, url запроса и из этой информации генерирует документацию. Также есть возможность задать описание параметров и. т. д.
Вот пример:

resource "Account" do
  get "/accounts" do
    parameter :page, "Page to view"

    # default :document is :all
    example "Get a list of all accounts" do
      do_request
      status.should == 200
    end

    # Don't actually document this example, purely for testing purposes
    example "Get a list on page 2", :document => false do
      do_request(:page => 2)
      status.should == 404
    end

    # With example_request, you can't change the :document
    example_request "Get a list on page 3", :page => 3 do
      status.should == 404
    end
  end

  post "/accounts" do
    parameter :email, "User email"

    example "Creating an account", :document => :private do
      do_request(:email => "eric@example.com")
      status.should == 201
    end

    example "Creating an account - errors", :document => [:private, :developers] do
      do_request
      status.should == 422
    end
  end
end

Что дальше?

Думаю, что основную информацию, необходимую для быстрого создания своего API-приложения в этом посте я предоставил.
Гемы, использованные в проекте, достаточно хорошо документированы. Если вы считаете, что я упустил что-то важное — пишите в комментариях или ЛС, я обязательно добавлю.
К следующему посту я постараюсь подготовить более-менее объективные бенчмарки данного приложения.
Пока могу сказать, что на i5 2500K один инстанс приложения (один thread) обрабатывает ~700 запросов в секунду к POST /api/auth/register с данными существующего пользователя.
Также в планах есть добавление поддержки JRuby на Goliath-сервере (уж больно там JIT хорош) и http и in-app кеширования.

Автор: AMar4enko

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js