zed.0xff.me
Особенности кэширования классов в Rails
Задача
Вот есть например у нас некий самописный класс для взаимодействия со сторонним API:
1 2 3 4 5 6 7 |
class SomeAPIClient def initialize params = {} @host = params[:host] || 'some.api.host' @port = params[:port] || 12345 end ... end |
И кладем мы его в #{RAILS_ROOT}/lib/, чтобы его можно было легко юзать из классов рельсового приложения.
Дальше мы задумываемся о том, что из нашего Rails-приложения, мы будем всегда обращаться к одним и тем же хосту и порту апи-сервера. И надо где-то их вконфигурить. А приложение-то у нас opensource, и хостится на github-e, поэтому хост и порт (и возможно какие-то другие параметры типа api-key) у нас для разных юзеров приложения могут быть разными.
Отсюда вопрос: Как бы это сконфигурить так, чтобы и либу не особо корежить, и чтоб наиболее Rails-way было?..
Мне вот лично пришла в голову следующая мысль:
1 2 3 4 5 6 7 8 9 |
class SomeAPIClient cattr_accessor :default_port, :default_host def initialize params = {} @host = params[:host] || default_host || 'some.api.host' @port = params[:port] || default_port || 12345 end ... end |
Проблема
Всё вроде бы красиво, свои значения для default_host & default_port мы назначаем в #{RAILS_ROOT}/config/initializers/my_api_client.rb, который исключаем из git-репозитария путём помещения в .gitignore.
Но! Но не зря в топике написано про кэширование классов.. в development окружении первый запрос к API выполняется отлично, а вот уже на втором и всех следующих – вступает в действие config.cache_classes = false, рельсы перечитывают декларацию нашего апи, при этом удаляя старый класс из памяти через remove_const. Ну а initializers естественно не перечитывают.
В production всё ОК, потому что там обычно config.cache_classes = true.
Решение
Копаясь во внутренностях Rails обнаружил вот это:
( actionpack-2.3.2/lib/action_controller/dispatcher.rb )
1 2 3 4 5 6 7 8 9 10 11 |
# Add a preparation callback. Preparation callbacks are run before every # request in development mode, and before the first request in production # mode. # # An optional identifier may be supplied for the callback. If provided, # to_prepare may be called again with the same identifier to replace the # existing callback. Passing an identifier is a suggested practice if the # code adding a preparation block may be reloaded. def to_prepare(identifier = nil, &block) ... |
Таким образом, чтобы решение стало работоспособным надо обернуть нашу инициализацию в do_prepare:
( #{RAILS_ROOT}/config/initializers/my_api_client.rb )
1 2 3 4 |
ActionController::Dispatcher.to_prepare(:my_api) do SomeAPIClient.default_host = 'my.api.host' SomeAPIClient.default_port = '23456' end |
И в development mode этот код будет срабатывать перед обработкой каждого запроса, а в production – только перед обработкой самого первого запроса.
Done.