10

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).

charliepark
  • 1,480
  • 2
  • 13
  • 15
  • @dgilperez the link is dead. – Ortwin Gentz Oct 19 '20 at 10:59
  • Yeah, thanks. I just deleted the comment. More context: my comment was 8y+ old, Rails 3 is long ago EoL and Sweepers are long ago no longer a thing :D - this case is probably ubiquitous in SO, I'm curious about what's the best way to deal with this from a historical perspective (removing comments also break history). – dgilperez Oct 19 '20 at 17:37

7 Answers7

11

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
  • 1,480
  • 2
  • 13
  • 15
  • 2
    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 Sep 29 '09 at 15:18
5

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
  • Thanks for this. Haven't tried it, but would love comments from anyone else who's tried this approach. – charliepark Jun 27 '10 at 01:58
  • 1
    This works if I modify MyModel through controller (browser). However this will not be called if I modify MyModel through console, since this bypass the controller, so the after_update was never registered to the model. – lulalala Jan 13 '12 at 07:11
3

I was experiencing the same problem when trying to do fragment caching (rails 3). Couldn't get the sweeper to observe, so I settled for the solution to make it an AR Observer as described above and calling ApplicationController.new.expire_fragment(...).

ZoogieZork
  • 11,215
  • 5
  • 45
  • 42
moiristo
  • 31
  • 1
2

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

  • 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 Jun 27 '10 at 01:56
  • How did u create init.rb file and route it in Rails app? – Shilpi Agrawal May 07 '15 at 11:30
  • I am getting this error undefined method `cache_sweeper' for NormalReservationsController:Class Did you mean? cache_store – Giridharan Oct 01 '19 at 12:09
0

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
  • 1,480
  • 2
  • 13
  • 15
0

I wrote a bit about this topic here: Rails Cache Sweeper Confusion. Would love to hear your opinions.

Nico
  • 881
  • 1
  • 6
  • 19
  • 1
    You just raised the question why it is not like you wished it to be. The blog post would help more if it: backlinks to this thread, explains the problem by examples, provides an insight to the rails core – Overbryd Oct 19 '11 at 15:20
  • 1
    The link is no longer valid. – traday Apr 18 '13 at 15:34
0

Based on @moiristo and @ZoogieZork 's answers, I am guessing this would work (untested).

class LinkSweeper < ActiveRecord::Observer
  include ActionController::Caching::Pages
  # or if you want to expire fragments
  #include ActionController::Caching::Fragments

  observe Link

  def after_update(link)
    expire_page( ... )
    #expire_fragment( ... )
  end
end
lulalala
  • 17,572
  • 15
  • 110
  • 169