views:

950

answers:

4

I'm working on a Rails app, where I'm using page caching to store static html output. The caching works fine. I'm having trouble expiring the caches, though.

I believe my problem is, in part, because I'm not expiring the cache from my controller. All of the actions necessary for this are being handled within the model. This seems like it should be doable, but all of the references to Model-based cache expiration that I'm finding seem to be out of date, or are otherwise not working.

In my environment.rb file, I'm calling

config.load_paths += %W( #{RAILS_ROOT}/app/sweepers )

And I have, in the /sweepers folder, a LinkSweeper file:

class LinkSweeper < ActionController::Caching::Sweeper
  observe Link

  def after_update(link)
    clear_links_cache(link)
  end

  def clear_links_cache(link)
  # expire_page :controller => 'links', :action => 'show', :md5 => link.md5
    expire_page '/l/'+ link.md5 + '.html'
  end
end

So ... why isn't it deleting the cached page when I update the model? (Process: using script/console, I'm selecting items from the database and saving them, but their corresponding pages aren't deleting from the cache), and I'm also calling the specific method in the Link model that would normally invoke the sweeper. Neither works.

If it matters, the cached file is an md5 hash off a key value in the Links table. The cached page is getting stored as something like /l/45ed4aade64d427...99919cba2bd90f.html.

Essentially, it seems as though the Sweeper isn't actually observing the Link. I also read (here) that it might be possible to simply add the sweeper to config.active_record.observers in environment.rb, but that didn't seem to do it (and I wasn't sure if the load_path of app/sweepers in environment.rb obviated that).

A: 

I've been able to get it to work, by way of adding

ActionController::Base.expire_page(app.link_path(:md5 => @link.md5))

to the method in the Model itself that's updating the database. This feels somewhat hacky, though, and I'd love to know if anyone can explain why it's not working with the normal sweeper setup, and if there's a more elegant way to handle this.

That snippet of code (apart from customizations I put in for my own app) came from this post on ruby-forum.com.

charliepark
A: 

So I've tried a number of different approaches, to see what works, and what doesn't.

Again, to summarize the situation: My goal is to expire cached pages when an object updates, but to expire them without relying on a Controller action. Conventional sweepers use a line in the controller to notify the sweeper that it needs to function. In this case, I can't use a line in the controller, as the update is happening within the model. Normal sweeper tutorials aren't working, as they presume that your main interaction with the database object is through the controller.

If, in reading this, you see a way to tighten up my code, please comment and let me know.

First, let's look at the things that DO work, in case you're stuck on this, too, and need help.

Of all the things I tried, the only thing that really seemed to work was to declare an after_update command in the Observer for the model. In that command, I used the explicit command for the expire_page action, and included a path that had been declared in routes.rb.

So. This works:

In config/routes.rb:

map.link 'l/:md5.:format',  :controller => 'links', :action => 'show'

In app/models/link_observer.rb:

def after_update(link)
  ActionController::Base.expire_page(app.link_path(:md5 => link.md5))
end

Note that that "md5" is specific to my app. You might want to use :id or some other unique identifier.

I also found that declaring that ActionController::Base... line from the method in the model that's doing the updating worked. That is, within Link.rb, in the method that's actually updating the database, if I just stuck that whole line in, it worked. But since I might want to expire that page cache on other methods in the future, I'd rather have it extracted into the Observer.

Now, let's look at some things that DID NOT work, in case you're Googling around for this.

Calling "expire_page(...)" within the after_update(link) method within link_observer.rb did not work, as it returned an "undefined method `expire_page'" error

Creating a Sweeper file that observed the Model did not work. I couldn't find any error codes, but it just seemed to not even be aware that it had a job to do. This was after explicitly calling "config.load_paths += %W( #{RAILS_ROOT}/app/sweepers )" within environment.rb. Just in case I fat-fingered something in that code, here it is:

class LinkSweeper < ActionController::Caching::Sweeper
  observe Link

  def after_update(link)
    clear_links_cache(link)
  end

  def clear_links_cache(link)
    # DID NOT WORK    expire_page :controller => 'links', :action => 'show', :md5 => link.md5
    # DID NOT WORK    expire_page '/l/'+ link.md5 + '.html'
    # DID NOT WORK    ActionController::Base.expire_page(app.link_path(:md5 => link.md5))
  end
end

That above example had the link_sweeper.rb file in a directory, /app/sweepers. I also tried putting link_sweeper.rb within the app/models directory, and tried calling it with the config.active_record.observers command in environment.rb:

config.active_record.observers = :link_observer, :link_sweeper

But that didn't work, either.

So, yeah. It's quite possible that one of these methods would work, and that I messed up something in the code. But I think I did everything by the book.

Ultimately, to summarize: Rather than using a Sweeper to expire page caching, you want to set up an after_ callback in the model's Observer. You'll want to use the explicit path to the Base.expire_page method:

def after_update(<model>) # where <model> is the name of the model you're observing
  ActionController::Base.expire_page(app.<model>_path(:id => <model>.id)) # where <model> is the name of the model you're observing
end

Hopefully this will help someone else down the road. Again, if you see anywhere in my not-working code where I should have done something differently, please let me know. If you see something in my working code that can be tighter, please let me know that, too.

charliepark
So all I had above this was working on my development server. When I tried it on the production server, however, it broke. I tried a number of alternate approaches, but none of them really worked. In the end, then, I rewrote that part of the app, so it runs the page expiration from a controller. Seems to be working for now.
charliepark
A: 

Just a note: you can use cache_sweeper in ApplicationController.

class ApplicationController < ActionController::Base
  cache_sweeper :my_sweeper
end

class MySweeper < ActionController::Caching::Sweeper
  observe MyModel

  def after_update(my_model)
    expire_page(...)
  end
end
Alexey Palazhchenko
Thanks for this. Haven't tried it, but would love comments from anyone else who's tried this approach.
charliepark
It worked for me.
Alexey Palazhchenko
A: 

Hi,

I did get this working. The only slight difference in my setup is that the sweeper is part of a Rails engine; which accounts for slight differences (loading the sweeper file with a require in the engine's init instead of adding it to the load path in environment.rb, etc).

So, the sweeper is loaded in the init.rb of the engine like this:

require File.join(File.dirname(__FILE__), 'app', 'sweepers', cached_category_count_sweeper')

I called it a sweeper because it "sweeps" the cache, but I guess its just an observer on the model:

class CachedCategoryCountSweeper < ActiveRecord::Observer
  observe CategoryFeature

  def before_save(cf)
    expire_cache(cf.category_id_was) if cf.category_id_changed?
  end

  def after_save(cf)
    expire_cache(cf.category_id)
  end

  def after_destroy(cf)
    expire_cache(cf.category_id)
  end

  def expire_cache(c)
    ApplicationController.expire_page("/categories/#{c}/counts.xml") if !c.nil?
  end
end

Frankly, I don't like having to hard-code the path, but I tried adding:

include ActionController:UrlWriter

and then using the path method, but it only worked for me in development. It didn't work in production, because my production server uses a relative url root (instead of virtual hosts) and the internal method "page_cache_path" would consistently get the file path wrong so it couldn't expire.

Since this is an observer, I added to the environment.rb:

config.active_record.observers = :cached_category_count_sweeper

Finally the controller that uses the cache (doesn't expire it, that is done through the model observer):

class CachedCategoryCountsController < ApplicationController
  caches_page :index

  # GET /cached_category_counts.xml
  def index
    ...
  end
end

Anyhow, hope this helps.

Andres Montano

Andres Montano
Thanks for working on this problem. I haven't tried the code out, but it sounds like you've done a thorough job testing it out. If anyone else tries this and has feedback, please leave it here. Would love to know how it works out (positively or negatively) for others.
charliepark