Running with Ruby

Tag: Views

Rails 3.2, Redis-store, views caching and expire_fragment with Regexp

In one of my projects, I have a VPS with low I/O, so I decided to move from disk cache to something else. Since I use some Regexps in expire_fragment method, I’ve decided to use Redis-store. It gives me exactly what I need:

  • Performance
  • Persistence
  • I already know Redis ;)
  • I use Redis in the same project for other purpose
  • “Almost” working Regexp support

expire_fragment with Regexp why aren’t you working?

After view minutes with Redis-store source code, I’ve figured out why ;) Well, it uses native Redis KEYS method to get all the matching keys and it expires them. Unfortunately KEYS don’t support Regexp matching :( Instead it works with wild-cards matching and the same goes for Redis-store.

Quick fix

Ok, this isn’t so bad. My Regexps are relatively simple and there’s not to much of them, so converting them should not be a problem. Most of the time I expire fragments outside controllers, so I’ve created an additional layer in the expire process (read more about this issue). We just need to map all the Regexps into an “Redis acceptable” form. As I mentioned above, my Regexps are simple, so mapping them was really easy (few examples):

/announcements-index/ => "*announcements-index*"
/weekly-topics-index/ => "*weekly-topics-index*"

Ruby code for such conversions looks like this:

fragment = "*#{fragment.to_s.split(':').last.gsub(')', '')}*"

this solution works for simple Regexps and it works for me. Unfortunately this isn’t the only issue with Redis-store. I’ve overwritten the expire_fragment method for my layer:

  def expire_fragment(fragment, options = nil)
    if Rails.configuration.cache_store == :redis_store
      if fragment.is_a?(Regexp)
        fragment = "*#{fragment.to_s.split(':').last.gsub(')', '')}*"
      end
    end
    super
  end

But still only direct cache hits expire would work.

cache_store.delete_matched doesn’t work?

Expire_fragment method under ActionController::Caching looks like this:

def expire_fragment(key, options = nil)
  return unless cache_configured?
  key = fragment_cache_key(key) unless key.is_a?(Regexp)

  instrument_fragment_cache :expire_fragment, key do
    if key.is_a?(Regexp)
      cache_store.delete_matched(key, options)
    else
      cache_store.delete(key, options)
    end
  end
end

So, as you can see, the delete_matched method is invoked only when we pass a Regexp. But hey! we never pass one :( we pass a string with a wild-card and it tries to expire it using the delete method. Luckily patching this is really simple:

module ActiveSupport
  module Cache
    class RedisStore < Store
      def delete(key, options)
        delete_matched(key, options)
      end
    end
  end
end

And that’s all :) After applying both presented here solutions, Redis-store should work with simple Regexps without any problems.

Controller stylesheet tag – style dla danego kontrolera

Tworząc arkusze stylów dla projektów często zdarza się, że chcemy dodać styl specyficzny dla danego kontrolera. Przykładowo w kontrolerze wyświetlającym pewien specyficzny rodzaj danych, chcemy mieć inne formatowanie. W przypadku Railsów składać się to będzie na dwie części – najpierw w ApplicationControlerze dodamy sobie metodę która ustawiać nam będzie trzy zmienne dostępne zarówno w kontrolerach jak i widokach:

  • @action_name – nazwa akcji
  • @controller_name – nazwa kontrolera
  • @module_name – nazwa modułu

Przy tym ostatnim (@module_name) należy pamiętać, że możemy mieć kilka “poziomów”, np: Admin::Core::Logs, tak więc będziemy to trzymać w tablicy ([Admin, Core, Logs]). Przyjmiemy też konwencję, że nazwy będą przechowywane z małej litery.

  def get_module_and_controller_name
    my_class_name = self.class.name
    @action_name = self.action_name
    if my_class_name.index("::").nil? then
      @module_name = nil
      @controller_name = self.class.to_s.split('Controller').first.underlinize
    else
      @controller_name = self.class.to_s.split("::").last.split('Controller').first.underlinize
      if (my_class_name.split("::").length > 2)
        @module_name = my_class_name.split("::")
        @module_name = @module_name[0, @module_name.length-1].collect{|m| m.underlinize}
      else
        @module_name = Array.new(1, my_class_name.split("::").first.underlinize)
      end
    end
  end

Taką metodę ustawiamy jako before_filter.

Teraz jeszcze tylko helper do widoków:

  def controller_stylesheet_link_tag(module_name, controller)

    if module_name.class.to_s.downcase == 'array'
      module_name = module_name.join('/')
    end
    ex = FileTest.exist?(File.join(Rails.root, 'public',
        'stylesheets', module_name, "#{controller}.css"))

    stylesheet_link_tag "#{module_name}/#{controller}" if ex
  end

oraz wywołanie:

controller_stylesheet_link_tag(@module_name, @controller_name)

Warto wspomnieć, że powyższe rozwiązanie działa na 3.0.7. Dla 3.1.0 będzie troszkę inaczej, ale to pozostawiam wam :)

Wersja dla Padrino jest troszkę mniej “fajna” (aczk. wymaga before_filtre’a):

  def controller_stylesheet_link_tag
    path = request.path.split('/')
    styles = ''
    while path.count > 0
      file = File.join(Padrino.root, 'app', 'stylesheets', 'controllers', path)+'.less'
      if File.exists?(file)
        styles += stylesheet_link_tag 'controllers'+path.join('/')
      end
      path.delete(path.last)
    end
    styles
  end

Copyright © 2018 Running with Ruby

Theme by Anders NorenUp ↑