Invoke cache sweeper through a rake task, which will loop over all the products in the database and expire the cache for each one. There is a fair amount of setup required for this to work. I will walk you through how to do this with a series of hacks.
The rake task
Create the rake task /lib/tasks/cache.rake which invokes ProductSweeper and calls the clear_all class method on it.
namespace :cache do desc 'Clears cache' task :clear => :environment do ProductSweeper.clear_all puts 'All caches cleared' end end
The Product Sweeper
I added the expire_all method to the product sweeper /app/sweepers/product_sweeper.rb, which would be invoked by the rake task.
class ProductSweeper observe Product def after_save(object) clear_cache(object) end def self.expire_all new.clear_all end def clear_all Product.all.each do |product| clear_cache product end end def clear_cache(product) expire_action :controller => '/product', :action => :show, :product => product end end
Testing it out
I ran the task and everything seemed to have worked fine. It printed ‘All caches cleared’, and there were no errors, but I expected to see the output below in the log file:
Expire fragment views/localhost:5000/products/testproduct
I double checked that it had indeed not cleared the caches by looking at the server log output when visiting the product on the front end. It kept on reading from the cache, and it was never invalidated. No errors to diagnose, it just failed silently.
So I fired up the debugger and followed the code execution path into Rails. You can get the debugger by adding the following to your Gemfile.
Then add add a method call to debug above the expire_cache method in the product sweeper.
Unsurprisingly it turns out that when a call is made to the expire_cache method on a class that inherits from ActionController::Caching::Sweeper, the first thing it hits is a method_missing method.
This source code looks like this looks like this:
def method_missing(method, *arguments, return unless @controller @controller.__send__(method, *arguments, &block) end
Note that it returns unless an instance variable named @controller exists. There’s our problem.
To get this to work, we need to find out what @controller should contain so that we can populate it ourselves. We can find this out by printing @controller while a sweeper is being invoked during normal operation. Meaning through an update, delete or create action on the ProductsController, printing out @controller yields a great wall of text which contains all the values stored in it. The important part is that the object contained in it is ProductsController, and as we know ProductsController inherits from ApplicationController.
Now that we know what that method_missing is looking for to send the message to the ApplicationController, we can modify the sweeper to create a new instance of ApplicationController and assign it to the @controller variable. We modify our ProductSweeper to include a new method named instantiate_controller which has the responsibility of populating @controller if it does not exist yet.
class ProductSweeper observe Product def after_save(object) clear_cache(object) end def self.expire_all new.clear_all end def clear_all instantiate_controller Product.all.each do |product| clear_cache product end end def clear_cache(product) expire_action :controller => '/product', :action => :show, :product => product end protected def instantiate_controller @controller ||= ApplicationController.new end end
After implementing this, we get a new error:
NoMethodError (undefined method 'host' for nil:NilClass)
After some research I found that manually populating the @controller.request.host value as demonstrated below will fix the error:
protected def instantiate_controller @controller ||= ApplicationController.new if @controller.request.nil? @controller.request = ActionDispatch::TestRequest.new @controller.request.host = Rails.application.config.host end end
Note that we pass in a fake Request object, and then set the value of the host on that.
With this hack in place, and after restarting the server, we finally get to see that message in the server log that we were looking for:
Expire fragment views/localhost:5000/products/testproduct (0.1ms)
The feature was now fully functional, and I could clear the caches on the command line with:
bundle exec rake cache:clear
This was also built into an ActiveAdmin dashboard, where a button could be pressed which would call the rake task. All of it was also successfully wrapped with Cucumber tests, so mission accomplished.
This application was built with Rails 3.2.13