Conditional Page Caching [Solution: Conditional Fragment Caching]

Conditional Page Caching [Solution: Conditional Fragment Caching]

cache_if and cache_unless seems to be the correct way to do this now:

cache_if(condition, name = {}, options = nil, &block)
cache_unless(condition, name = {}, options = nil, &block)

Your code:

<% cache_unless @user.changed?, [ @user, 'form' ] do %>

How do you show cached pages to only guests in Rails?

To quote Rails Guides

One of the issues with Page Caching is that you cannot use it for
pages that require to restrict access somehow. This is where Action
Caching comes in. Action Caching works like Page Caching except for
the fact that the incoming web request does go from the webserver to
the Rails stack and Action Pack so that before filters can be run on
it before the cache is served. This allows authentication and other
restriction to be run while still serving the result of the output
from a cached copy.

So, simple answer is you should use this instead

caches_action :show, :unless => :user_signed_in?

so that authenticate_user! before filter is able to run first.

How to expire conditional GET cache when locale changes?

The solution was simple. But works only with the etags-method:

class BalancesController < ApplicationController
etag { current_locale }

def mine
fresh_when(etag: current_user.balance)
end

private
def current_locale
@locale ||= locale_from_headers
end
end

If-Modified-Since

With the If-Modified-Since method, it is not possible to expire the cache. Since Rails does not store any cache when using the Conditional GET but only compares passed in timestamps with timestamps on the objects.

Neither of those have the ability to carry more information than "Just a Date". We would need rails to store its caches (like with fragment caching) in order to also store the language it was created for. This method will not allow us to expire a cache based on the language header.

By passing only the etag option, the if-modified-since headers are ignored: fresh_when(etag: current_user.balance)

If-None-Match

This represents an entity-tag, and is an identifier for the content being served. Usually this is built using the object-id of the object being returned.

Conditional Get allows extra etag-builders, named etaggers to be defined.

This can be used to add extra information into the entity-tag.

Conditional layout and action caching without a layout

I almost think this might be a bug in Rails. Check out this block of code around line 143 of actionpack/lib/action_controller/caching/actions.rb:

body = controller.read_fragment(cache_path.path, @store_options)

unless body
controller.action_has_layout = false unless @cache_layout
yield
controller.action_has_layout = true
body = controller._save_fragment(cache_path.path, @store_options)
end

body = controller.render_to_string(:text => body, :layout => true) unless @cache_layout

controller.response_body = body

It looks like it's correctly rendering the body without a layout in the first unless block, but then it's forcing the template to render with a layout as part of the response body. And if you look at the stack trace, that's the line that leads to the exception.

I manually edited the file to :layout => @cache_layout (which always evaluates to :layout => false since it's guarded by an unless) and the view rendered as expected.

I'm not sure what you could do about this other than temporarily patch that file yourself and open a bug report. I might also be wrong about the behavior of that line, but it certainly looks like the culprit.

Fragment caching & CSRF

Here is an article describing different methods: http://www.fastly.com/blog/Caching-the-Uncacheable-CSRF-security/.

A short summary:

  1. Using ESI (Edge Side Includes): render a placeholder in Rails which you fill with the CSRF token later.
  2. Including the CSRF token in a cookie and copy it into the form via javascript.
  3. Fetch the token in a separate AJAX request and copy it into the form via javascript.

My take:

You need to set up special infrastructure for using ESI, so I do not like that solution. AJAX requests are slow and a lot of network overhead, so I do not like that solution as well...
So I would go with the cookie solution or with the JS solution you already mentioned, since these are the simplest solutions.

Page caching using delayed job

To accomplish timed page caching you can utilize the standard Rails' caching plus a little timed cleverness.

First you want to determine your level of caching. You've got three options:

  1. Page Caching - caches the whole page but subsequent requests don't go to through Rails stack. So if this is a Craiglist-esque page that will be hit thousands of times a second this request will only go to your webserver (e.g. apache) not Rails or your db making it much faster. The trade off is that you lose authentication, session variables, etc that Rails provides.
  2. Action Caching - caches the whole page but brings the request into Rails so that it can execute any filters associated with that action.
  3. Fragment Caching - caches a segment of the page, essentially bypassing the need to execute the code with in the block (and any consequential calls to the DB).

Then you'll need pick the appropriate level of caching and implement it in your app (check out the links above for implementation examples).

Once you have implemented the caching you now have to figure out a way to expire the cache. I can think of two ways to do this, both come with benefits and drawbacks. For now let's assume you've chosen to use action caching.

  1. Reliable but more involved - create an action within your controller that expires the cache and a cron job task that makes a request to that action. I've asked a similar question that addresses this 'built in' scheduled task. For security precautions, you may want to include a generated hash or something similar so that someone can't manually expire your cache by going to '/products/expire_cache'.

    class ProductsController < ApplicationController
    caches_action :index

    def index
    # implementation
    end

    def expire_cache
    if params[:verification_hash] == 'sa89sf8sfsfehiwaf89yfea98fh'
    expire_action :action => :index
    end
    end
    end
  2. Unreliable but easier - simply expire the cache in your action with an arbitrary conditional. This implementation assumes that there will be enough traffic to regularly ensure that someone will come to your site on the 0, 15, 30, and 45 minutes. You could decrease this interval in order ensure that the cache will be reset at a more probable interval.

    class ProductsController < ApplicationController
    caches_action :index

    def index
    # implementation
    expire_action :action => :index if Time.now.min % 15 == 0
    end
    end


Related Topics



Leave a reply



Submit