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:
- Using ESI (Edge Side Includes): render a placeholder in Rails which you fill with the CSRF token later.
- Including the CSRF token in a cookie and copy it into the form via javascript.
- 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:
- 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.
- Action Caching - caches the whole page but brings the request into Rails so that it can execute any filters associated with that action.
- 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.
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
endUnreliable 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
How to Replace the Characters in a String
Why Is the << Operation on an Array in Ruby Not Atomic
Ruby: How to Call Function Before It Is Defined
Trouble Installing Ruby 1.9.2 with Rvm MAC Os X
Ruby 'Gets' That Works Over Multiple Lines
How to Use Headless Chrome with Capybara and Selenium
How to Convert a Ruby Bigdecimal to a 2-Decimal Place String
Undefined Method 'Instance' for Capistrano::Configuration:Class
Call Next on Ruby Loop from External Method
Rails: Pundit::Authorizationnotperformederror
Marshal Ruby Hash with Default Proc - Remove the Default Proc
How to Enable Colors with Rspec When Using Jruby or Bundle Exec
Ruby on Rails: Converting "Somewordhere" to "Some Word Here"
Selenium2 Webdriver Ruby => How Click on a Hidden Link