Heroku (что это и почему засыпают воркеры)
heroku.com — замечательный сервис, который позволяет писать на ruby не заботясь о том, как выкатывается новая версия, как делается дамп бд, где собрать статистику. Но, как во всех бочках меда, тут есть ложка дегтя, даже две:
Первая — при использовании его как полноценного хостинга под большие проекты — стоит жутко дорого;
Вторая — при использовании в качестве “бесплатной площадки для игр” предоставляет соответствующие размеры ресурсов (1 веб-воркер).
Первое, на что я наткнулся — это невозможность запуска в одном проекте веб-воркера и обработчика фоновых задач. Это можно обойти, используя в качестве сервера unicorn, но сразу же возникает новая проблема.
Например: Один воркер, бесплатный. Если использовать sidekiq в качестве менеджера очереди сообщений и добавлять задачи в очередь (redis) через rake задачи + scheduller (для них heroku выделяет отдельный воркер), то рано или поздно, а точнее через час, мастер-процесс unicorn’а заснет…
Для решения этой проблемы было решено написать простой гем, который можно будет использовать во всех проектах.
Встречаем WakeUpNeo: https://github.com/england/wake_up_neo
Дальше я опишу то, что использовалось при разработке.
Bundler
С помощью одной команды можно создать базовую структуру файлов/папок гема (так же эта команда инициализирует git репозиторий):
bundle gem wake_up_neo
Bundler — инструмент для управления гемами и их зависимостями (dependencies) в ruby-приложении.
Файл wake_up_neo/wake_up_neo.gemspec как раз и хранит зависимости, в нашем случае это:
# -*- encoding: utf-8 -*- require File.expand_path('../lib/wake_up_neo/version', __FILE__) Gem::Specification.new do |gem| gem.authors = ["england"] gem.email = ["englandpost@gmail.com"] gem.summary = gem.summary = %q{Simple trick to prevent... gem.homepage = "https://github.com/england/wake_up_neo" gem.add_dependency('rails', '>= 3.0.0') gem.files = `git ls-files`.split($) gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.name = "wake_up_neo" gem.require_paths = ["lib"] gem.version = WakeUpNeo::VERSION end
Гемы, необходимые только для разработки и тестирования, стоит добавить через add_development_dependency, чтобы не они не попали в production окружение. В этом же файле хранится информация об авторе и краткое описание гема.
Generator
Генераторы в Rails основаны на Thor, который предоставляет шикарный api для манипуляций с файлами.
Нам нужно только создать простой initializer для назначения url’а, за который будет дергаться приложение на heroku.
Для этого достаточно одной команды create_file, где первый параметр — это путь к файлу, а второй — текст.
module WakeUpNeo module Generators class InstallGenerator < Rails::Generators::Base desc "Creates initializer file" def create_initializer_file create_file 'config/initializers/wake_up_neo.rb', <<-CONTENT WakeUpNeo.configure do |config| # Url what will touch # For example 'http://myapp.heroku.com' config.knock_knock_url = '' end CONTENT end end end end
Повинуясь autoload’у, команда rails g wake_up_neo:install будет доступна из консоли и создаст файл config/initializers/wake_up_neo.rb.
(Посмотреть, что еще умеет Thor можно на http://rdoc.info/github/wycats/thor/master/Thor/Actions.html)
Initializer
“Здесь нужен блок.”
Галустян.
И хотя в модуле пока не планируется никаких сложных настроек, все же передать блок методу configure очень хочется. Блок позволяет выполнить код в контексте исполняемого метода. В нашем случае устанавливается значение instance переменной @knock_knock_url.
module WakeUpNeo module Config attr_accessor :knock_knock_url def configure yield self end def knock_knock_url if @knock_knock_url && !@knock_knock_url.blank? @knock_knock_url else raise "You must specify knock_knock_url in config/initializers/wake_up_neo.rb" end end end end
Дальше остается только “расширить” модуль на пару методов.
module WakeUpNeo extend Config def self.knock_knock open(knock_knock_url) end end
Rake Task
Итак, мы приближаемся к цели — rake-задаче, которая будет выполняться heroku scheduler’ом и вызывать метод, дергающий проект за nginx и не дающий заснуть.
namespace :wake_up_neo do desc 'Touch knock_knock_url' task knock_knock: :environment do WakeUpNeo.knock_knock end end
Выглядит она просто. В принципе, от нее другого и не требуется. Есть только необходимость, чтобы она была в списке задач rails-приложения. Здесь нужен контрудар.
Railtie
Railtie — ядро rails, и его расширение позволяет взаимодействовать с фреймворком во время загрузки. Унаследовав класс от Rails::Railtie, мы получаем возможность добавлять инициализаторы, изменять config приложения, добавлять генераторы (то, что я описывал выше, только лучше) и, конечно, добавлять rake-задачи.
module WakeUpNeo class Railtie < Rails::Railtie rake_tasks do load "tasks/wake_up_neo.rake" end end end
Теперь в списке rake -T есть
rake wake_up_neo:knock_knock
— можно добавлять ее в sheduller на выполнение с интервалом раз в 10 минут, и наше приложение точно никогда не уснет.