Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Agile Web Development With Rails, 2nd Edition (2006).pdf
Скачиваний:
30
Добавлен:
17.08.2013
Размер:
6.23 Mб
Скачать

CACHING, PAR T ONE 455

:redirect_to =>params

Redirects using the given parameter hash.

:render =>params

Renders using the given parameter hash.

21.5Caching, Part One

Many applications seem to spend a lot of their time doing the same task over and over. A blog application renders the list of current articles for every visitor. A store application will display the same page of product information for everyone who requests it.

All this repetition costs us resources and time on the server. Rendering the blog page may require half a dozen database queries, and it may end up running through a number of Ruby methods and Rails templates. It isn’t a big deal for an individual request, but multiply that by many a thousand hits an hour, and suddenly your server is starting to glow a dull red. Your users will see this as slower response times.

In situations such as these, we can use caching to greatly reduce the load on our servers and increase the responsiveness of our applications. Rather than generate the same old content from scratch, time after time, we create it once and remember the result. The next time a request arrives for that same page, we deliver it from the cache, rather than create it.

Rails offers three approaches to caching. In this chapter, we’ll describe two of them, page caching and action caching. We’ll look at the third, fragment caching, on page 513 in the Action View chapter.

Page caching is the simplest and most efficient form of Rails caching. The first time a user requests a particular URL, our application gets invoked and generates a page of HTML. The contents of this page are stored in the cache. The next time a request containing that URL is received, the HTML of the page is delivered straight from the cache. Your application never sees the request. In fact, Rails is not involved at all: the request is handled entirely within the web server, which makes page caching very, very efficient. Your application delivers these pages at the same speed that the server can deliver any other static content.

Sometimes, though, our application needs to be at least partially involved in handling these requests. For example, your store might display details of certain products only to a subset of users (perhaps premium customers get earlier access to new products). In this case, the page you display will have the same content, but you don’t want to display it to just anyone—you need to filter access to the cached content. Rails provides action caching for this purpose.

Report erratum

CACHING, PAR T ONE 456

With action caching, your application controller is still invoked, and its before filters are run. However, the action itself is not called if there’s an existing cached page.

Let’s look at this in the context of a site that has public content and premium, members-only, content. We have two controllers, a login controller that verifies that someone is a member and a content controller with actions to show both public and premium content. The public content consists of a single page with links to premium articles. If someone requests premium content and they’re not a member, we redirect them to an action in the login controller that signs them up.

Ignoring caching for a minute, we can implement the content side of this application using a before filter to verify the user’s status and a couple of action methods for the two kinds of content.

Download e1/cookies/cookie1/app/controllers/content_controller.rb

class ContentController < ApplicationController

before_filter :verify_premium_user, :except => :public_content

def public_content

@articles = Article.list_public end

def premium_content

@articles = Article.list_premium end

private

def verify_premium_user user = session[:user_id]

user = User.find(user) if user unless user && user.active?

redirect_to :controller => "login", :action => "signup_new" end

end end

Because the content pages are fixed, they can be cached. We can cache the public content at the page level, but we have to restrict access to the cached premium content to members, so we need to use action-level caching for it. To enable caching, we simply add two declarations to our class.

Download e1/cookies/cookie1/app/controllers/content_controller.rb

class ContentController < ApplicationController

before_filter :verify_premium_user, :except => :public_content

caches_page :public_content caches_action :premium_content

Report erratum

CACHING, PAR T ONE 457

The caches_page directive tells Rails to cache the output of public_content the first time it is produced. Thereafter, this page will be delivered directly from the web server.

The second directive, caches_action, tells Rails to cache the results of executing premium_content but still to execute the filters. This means that we’ll still validate that the person requesting the page is allowed to do so, but we won’t actually execute the action more than once.14

Caching is, by default, enabled only in production environments. You can turn it on or off manually by setting

ActionController::Base.perform_caching = true | false

You can make this change in your application’s environment files (in config/environments), although the preferred syntax is slightly different there.

config.action_controller.perform_caching = true

What to Cache

Rails action and page caching is strictly URL based. A page is cached according to the content of the URL that first generated it, and subsequent requests to that same URL will return the saved content.

This means that dynamic pages that depend on information not in the URL are poor candidates for caching. These include the following.

Pages where the content is time based (although see Section 21.5, TimeBased Expiry of Cached Pages, on page 461).

Pages whose content depends on session information. For example, if you customize pages for each of your users, you’re unlikely to be able to cache them (although you might be able to take advantage of fragment caching, described starting on page 513).

Pages generated from data that you don’t control. For example, a page displaying information from our database might not be cachable if nonRails applications can update that database too. Our cached page would become out-of-date without our application knowing.

However, caching can cope with pages generated from volatile content that’s under your control. As we’ll see in the next section, it’s simply a question of removing the cached pages when they become outdated.

14. Action caching is a good example of an around filter, described on page 450. The before part of the filter checks to see whether the cached item exists. If it does, it renders it directly back to the user, preventing the real action from running. The after part of the filter saves the results of running the action in the cache.

Report erratum

CACHING, PAR T ONE 458

Expiring Pages

Creating cached pages is only one half of the equation. If the content initially used to create these pages changes, the cached versions will become out-of- date, and we’ll need a way of expiring them.

The trick is to code the application to notice when the data used to create a dynamic page has changed and then to remove the cached version. The next time a request comes through for that URL, the cached page will be regenerated based on the new content.

Expiring Pages Explicitly

The low-level way to remove cached pages is with the methods expire_page and expire_action. These take the same parameters as url_for and expire the cached page that matches the generated URL.

For example, our content controller might have an action that allows us to create an article and another action that updates an existing article. When we create an article, the list of articles on the public page will become obsolete, so we call expire_page, passing in the action name that displays the public page. When we update an existing article, the public index page remains unchanged (at least, it does in our application), but any cached version of this particular article should be deleted. Because this cache was created using caches_action, we need to expire the page using expire_action, passing in the action name and the article id.

Download e1/cookies/cookie1/app/controllers/content_controller.rb

def create_article

article = Article.new(params[:article])

if article.save

 

expire_page

:action => "public_content"

else

 

# ...

 

end

 

end

 

def update_article

article = Article.new(params[:article]) if article.save

expire_action :action => "premium_content", :id => article else

# ...

end end

The method that deletes an article does a bit more work—it has to both invalidate the public index page and remove the specific article page.

Report erratum

CACHING, PAR T ONE 459

Download e1/cookies/cookie1/app/controllers/content_controller.rb

def delete_article Article.destroy(params[:id])

expire_page

:action => "public_content"

expire_action :action => "premium_content", :id => params[:id] end

Picking a Caching Store Strategy

Caching, like sessions, features a number of storage options. You can keep the fragments in files, in a database, in a DRb server, or in memcached servers. But whereas sessions usually contain small amounts of data and require only one row per user, fragment caching can easily create sizeable amounts of data, and you can have many per user. This makes database storage a poor fit.

For many setups, it’s easiest to keep cache files on the filesystem. But you can’t keep these cached files locally on each server, because expiring a cache on one server would not expire it on the rest. You therefore need to set up a network drive that all the servers can share for their caching.

As with session configuration, you can configure a file-based caching store globally in environment.rb or in a specific environment’s file.

ActionController::Base.fragment_cache_store =

ActionController::Caching::Fragments::FileStore.new( "#{RAILS_ROOT}/cache" )

This configuration assumes that a directory named cache is available in the root of the application and that the web server has full read and write access to it. This directory can easily be symlinked to the path on the server that represents the network drive.

Regardless of which store you pick for caching fragments, you should be aware that network bottlenecks can quickly become a problem. If your site depends heavily on fragment caching, every request will need a lot of data transferring from the network drive to the specific server before it’s again sent on to the user. In order to use this on a high-profile site, you really need to have a highbandwidth internal network between your servers or you will see slowdown.

The caching store system is available only for caching actions and fragments. Full-page caches need to be kept on the filesystem in the public directory. In this case, you will have to go the network drive route if you want to use page caching across multiple web servers. You can then symlink either the entire public directory (but that will also cause your images, stylesheets, and JavaScript to be passed over the network, which may be a problem) or just the individual directories that are needed for your page caches. In the latter case, you would, for example, symlink public/products to your network drive to keep page caches for your products controller.

Report erratum

CACHING, PAR T ONE 460

Expiring Pages Implicitly

The expire_xxx methods work well, but they also couple the caching function to the code in your controllers. Every time you change something in the database, you also have to work out which cached pages this might affect. Although this is easy for smaller applications, this gets more difficult as the application grows. A change made in one controller might affect pages cached in another. Business logic in helper methods, which really shouldn’t have to know about HTML pages, now needs to worry about expiring cached pages.

Fortunately, Rails sweepers can simplify some of this coupling. A sweeper is a special kind of observer on your model objects. When something significant happens in the model, the sweeper expires the cached pages that depend on that model’s data.

Your application can have as many sweepers as it needs. You’ll typically create a separate sweeper to manage the caching for each controller. Put your sweeper code in app/models.

Download e1/cookies/cookie1/app/sweepers/article_sweeper.rb

class ArticleSweeper < ActionController::Caching::Sweeper

observe Article

#If we create a new article, the public list of articles must be regenerated def after_create(article)

expire_public_page end

#If we update an existing article, the cached version of that article is stale def after_update(article)

expire_article_page(article.id) end

#Deleting a page means we update the public list and blow away the cached article def after_destroy(article)

expire_public_page expire_article_page(article.id)

end

private

def expire_public_page

expire_page(:controller => "content", :action => 'public_content') end

def expire_article_page(article_id) expire_action(:controller => "content",

:action

=>

"premium_content",

:id

=>

article_id)

end

 

 

end

 

 

Report erratum

CACHING, PAR T ONE 461

The flow through the sweeper is somewhat convoluted.

The sweeper is defined as an observer on one or more Active Record classes. In our example case it observes the Article model. (We first talked about observers back on page 379.) The sweeper uses hook methods (such as after_update) to expire cached pages if appropriate.

The sweeper is also declared to be active in a controller using the directive cache_sweeper.

class ContentController < ApplicationController

before_filter :verify_premium_user, :except => :public_content caches_page :public_content

caches_action :premium_content

cache_sweeper :article_sweeper,

:only => [ :create_article, :update_article, :delete_article ]

#...

If a request comes in that invokes one of the actions that the sweeper is filtering, the sweeper is activated. If any of the Active Record observer methods fires, the page and action expiry methods will be called. If the Active Record observer gets invoked but the current action is not selected as a cache sweeper, the expire calls in the sweeper are ignored. Otherwise, the expiry takes place.

Time-Based Expiry of Cached Pages

Consider a site that shows fairly volatile information such as stock quotes or news headlines. If we did the style of caching where we expired a page whenever the underlying information changed, we’d be expiring pages constantly. The cache would rarely get used, and we’d lose the benefit of having it.

In these circumstances, you might want to consider switching to time-based caching, where you build the cached pages exactly as we did previously but don’t expire them when their content becomes obsolete.

You run a separate background process that periodically goes into the cache directory and deletes the cache files. You choose how this deletion occurs—you could simply remove all files, the files created more than so many minutes ago, or the files whose names match some pattern. That part is application-specific.

The next time a request comes in for one of these pages, it won’t be satisfied from the cache and the application will handle it. In the process, it’ll automatically repopulate that particular page in the cache, lightening the load for subsequent fetches of this page.

Report erratum