Tag: cache

Clear memcached without restart with Ruby and Capistrano (Rake task)

After successful Capistrano update (cap deploy) if we use Memcached - we should clear it. How can we do it without root access to the server? There are two ways to clear memcached without restarting it:

  • Memcached command: flush_all
  • Rails.cache.clear

One flush to rule them all - flush_all

If we have a dedicated server with one application on it - we can clear whole memcached memory, to do so, we create a rake task (/lib/tasks/memcached.rake):

require 'socket'

namespace :memcached do
  desc 'Flushes whole memcached local instance'
  task :flush do
    server  = '127.0.0.1'
    port    = 11211
    command = "flush_all\r\n"

    socket = TCPSocket.new(server, port)
    socket.write(command)
    result = socket.recv(2)

    if result != 'OK'
      STDERR.puts "Error flushing memcached: #{result}"
    end

    socket.close
  end
end

Usage:

bundle exec rake memcached:flush

It is worth mentioning, that this task doesn't require the Rails environment to be loaded. If it goes about the server address and port - you can always modify it so it will accept the env settings instead of hardcoding it.

Be aware, that this command will clear out all the data that is stored in Memcached instance, even the data that was used by other applications (other than our). If you want to clear out data used by one of many apps that are using same Memcached server, see the solution presented below.

Clearing single Rails app memcached data - Rails.cache.clear

Apart from flushing all the data that is in Memcached, we can always clear only the Rails cache by creating a really simple rake task (/lib/tasks/memcached.rake):

namespace :memcached do
  desc 'Clears the Rails cache'
  task :flush => :environment do
    Rails.cache.clear
  end
end

The execution process is exactly like in the previous case:

bundle exec rake memcached:flush

In this Rake task we do load the Rails environment (because we want to use Rails.cache instance). In multi application environment, this Memcached cleaning method seems way better because we work with our application scope only.

Capistrano task for clearing Memcached

So we have our rake task, but it would mean nothing without a Capistrano hookup:

namespace :memcached do

  desc "Flushes memcached local instance"
  task :flush, :roles => [:app] do
    run("cd #{current_path} && rake memcached:flush")
  end

end

Now we can use it like this:

bundle exec cap memcached:flush

Or we can hookup it to update process:

after 'deploy:update' do
  memcached.flush
end

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.

Copyright © 2024 Closer to Code

Theme by Anders NorenUp ↑