8.7. Maintaining the State

It is fair to state that each request-response pair is a world unto itself. For example, an instance variable set within the index action is not going to be available in the view when serving a second request for the edit action.

NOTE

By the way, go easy on the number of instance variables that you define in each action. These are expensive and introducing an excessive number of instance variables is considered to be faux pas. If a variable within a controller is not going to be used by the view, declare it as a local variable (that is, don't prefix it with a @ sign).

The stateless nature of the Web poses the challenging question of how to maintain the state. This could be something as simple as carrying a message over from an action to another when a redirect is required, or a more elaborate solution to keep track of which users are currently logged in, without prompting them to re-enter their username and password at each new request for a protected page.

In Rails, the first scenario is resolved through a flash object, and the second by employing sessions.

8.7.1. Flash

As mentioned previously, instance variables defined within an action are not available to requests that are serving a different action. The reason for this is that with each request, a brand new instance of the controller is generated. This implies that instance variables cannot be used as a means to pass messages between actions whenever you are performing a redirect (which is, for all practical purposes, a new request). For example, the following snippet will not work because @message will be nil within the confirm action:

# Don't do this
def create
  @email = Email.new(params[:email])
  if @email.save
    @message = 'Your email has been subscribed to our newsletter'
    redirect_to(:action => "confirm")
  else
    render(:action => "new")
  end
end

def confirm
  render :text => @message
end

Flash is a hash-like object that has the peculiar characteristic of making its content available until the next request has been processed. Because of this, it's possible to store informative and error messages that you intend to display in the action that you are redirecting to. For example, this is the code for the create method in the ArticlesController of the blog application:

def create
  @article = Article.new(params[:article])

  respond_to do |format|
    if @article.save
      flash[:notice] = 'Article was successfully created.'
      format.html { redirect_to(@article) }
      format.xml  { render :xml => @article, :status => :created,
                                             :location => @article }

else
      format.html { render :action => "new" }
      format.xml  { render :xml => @article.errors,
                           :status => :unprocessable_entity }
    end
  end
end

When serving a regular request that comes from a browser, the :notice "key" of the hash-like flash object is assigned a confirmation message before redirecting to the show action for the article that was just created. That message is then retrieved by the view, while processing the next action (show). In fact, in this case (and by default when scaffolding), you render the value of flash[:notice] in green within the articles.html.erb layout:

...
<div id="main" class="container">
    <p style="color: green" id="notice"><%= flash[:notice] %>
    <%= yield  %>
</div>
...

The evaluation of this value could have been placed in any view (for example, show.html.erb), but it's usually convenient to make it available for all the actions of a controller, by positioning it within the layout just before the yield method.

When the show action is finally processed and the message has been displayed, flash will automatically clear its content. You can choose any key (for example, flash[:confirmation]), but the most common ones in Rails projects are :notice, :warning, and :error.

The object offers a few methods to alter the default behavior. discard enables you to mark the flash to be swiped at the end of the current action. You can either discard the whole object or just one entry:

flash.discard         # Discard all
flash.discard(:error) # Discard :error only

now sets the flash so that it's available for the current action only. You can use it as follows:

flash.now[:notice] = "You're still in the current action!"

Having used now doesn't affect the way the flash entries are retrieved in the view (for example, flash[:notice]).

Finally, the method keep allows you to extend the lifespan of the flash entries for one more action. Just like discard, you can scope the method to a single entry:

flash.keep          # Keep all the entries
flash.keep(:notice) # Keep the :notice entry only

Behind the scenes, both discard and keep use the same method, namely use, which is a private method that allows them to mark flash entries as already having been used (for discard) or as unused (for keep).

Any serializable object can be stored in the flash, but it's highly recommended that you keep it simple, and limit the contents to messages (strings) or parameter passing (to the next action).

The flash is stored in the session data, and as such it requires that the sessions are enabled (which they are by default) to work properly. The flash mechanism could easily be emulated by saving the message in the session directly. Unlike saving the messages in a key within the session though, flash is handy because it automatically clears itself after a response has been provided for the next request, whereas objects in the session stick around until the session data has been explicitly removed or the session has expired.

8.7.2. Sessions

Because HTTP is a stateless protocol, Rails sessions are used to persist the state across several actions/requests. If you've done Web development before, this concept should not be new to you. For example, if you are familiar with the ASP.NET session state, you know that session variables are stored in a SessionStateItemCollection object, which in turn is exposed within a WebForms page as its Session property.

Setting session variables in ASP.NET is fairly straightforward thanks to Session:

Visual Basic (.NET)

Dim firstName As String = "Antonio"
Dim lastName As String = "Cangiano"
Session("FirstName") = firstName
Session("LastName") = lastName

C#

string firstName = "Antonio";
string lastName = "Cangiano";
Session["FirstName"] = firstName;
Session["LastName"] = lastName;

And so is retrieving them:

Visual Basic (.NET)

Dim firstName as String = CType(Session.Item("FirstName"), String)
Dim lastName as String = CType(Session.Item("LastName"), String)

C#

string firstName = (string)(Session["FirstName"]);
string lastName = (string)(Session["LastName"]);

In the Rails world, session data is stored in a collection-like object too (or hash-like in Ruby speak), which is accessible through the session (lowercase) attribute of each controller. Setting the same values outlined previously in Rails is done as follows:

session[:first_name] = "Antonio"
session[:last_name] = "Cangiano"

And retrieving values set elsewhere is just as easy, like if you were dealing with a regular hash:

first_name = session[:first_name]
last_name = session[:last_name]

session's keys can be strings, but it's idiomatically preferred to use symbols. The distinction between strings and symbols is explained in Chapter 3.

What to Store in the Session?

Though sessions can store any serializable object (those that can be marshaled and unmarshaled through Ruby's Marshal.dump and Marshal.load methods), it is a good idea to try to keep your session data's size to a minimum. Instead of storing a whole ActiveRecord object (which is a very bad practice to get in the habit of), just store its id. Don't store too much data (as you will see, the cookie-based sessions are limited to 4Kb) and try to avoid storing important information within the session before it has been safely persisted in the database.


Like the ASP.NET session state, Rails sessions are identified by a unique id known as the session id. This is randomly generated as a 32-hexadecimal characters long value, and just like the SessionID property in ASP.NET, this too is stored in a cookie by default, ready to be retrieved at each request to identify which session data belongs to which request.

The cookies generated by Rails are crypto-signed, to ensure that their content is valid and that they haven't been tampered with by a malicious user (even though their content is not encrypted and is therefore visible in clear text). Furthermore, Rails 2.2 increases the security of your projects by setting by the default option :session_http_only to true. This disables JavaScript from having access to the cookies.

The fact that the session_id key is assigned 32 hexadecimal characters implies that a Rails application is able to uniquely identify up to 340282366920938463463374607431768211456 different active sessions (that's 16 to the 32nd power).

When dealing with sessions, the most important question to ask is where to store the session data.

8.7.3. Session Storage Options

Rails offers several session storage options, each of which has its own pros and cons, depending on your application type, traffic, and deployment arrangements.

8.7.3.1. ActiveRecordStore

With the ActiveRecordStore session data is marshaled in Base64 format and stored in a data column within a sessions table in the database by default. That table has a session_id text field and, by convention, the usual id primary key. As usual, these defaults can be overwritten if need be. By default the table is defined with the automatically handled updated_at and created_at attributes, which makes implementing session expiration fairly easy.

Run rake db:sessions:create to create a migration file for the sessions table. The generated file will then be defined as follows:

class CreateSessions < ActiveRecord::Migration
  def self.up
    create_table :sessions do |t|
      t.string :session_id, :null => false

t.text :data
      t.timestamps
    end

    add_index :sessions, :session_id
    add_index :sessions, :updated_at
  end

  def self.down
    drop_table :sessions
  end
end

Notice how session_id is not nullable, a default id column is generated, and indexes are defined for both session_id and updated_at to improve performance.

At this point, all you have to do is migrate (rake db:migrate) and set active_record_store as the session storage for the application in configenvironment.rb. There is a commented out line for this particular option already, so all you have to do it is uncomment it:

config.action_controller.session_store = :active_record_store

That symbol corresponds to the class CGI::Session::ActiveRecordStore. Like the other options you'll see in a moment, the symbol used for setting the session store is always the snake_case version of the class name.

When you develop, at times you may need to clear all the sessions. To do so quickly and easily, use the handy rake db:sessions:clear.

8.7.3.2. CookieStore

CookieStore is Rails' default session storage. Unlike ActiveRecordStore, where the session data is stored server side in the database, CookieStore stores the data client-side in cookies.

We can only assume that this would be his favorite session storage option if Cookie Monster were to ever entertain the idea of programming in Rails.

The session data stored in cookies is limited to 4096 bytes. The user cannot modify the content of a cookie, though (for example, altering a user_id variable to access other people's session data). In fact, each cookie includes a message digest and the integrity of the cookie is checked through the SHA1 algorithm. This can be changed to other digests supported by OpenSSL (for example, SHA256, MD5, and so on).

NOTE

In environment.rb you'll find a :secret option set for verifying cookie-based sessions. This is randomly generated for you and needs to be at least 30 characters long. Don't replace it with common words, or it will be vulnerable to dictionary attacks. If you are porting a legacy application to Rails 2.2, you can use rake secret to have a cryptographically secure key generated for you.

Because this is Rails' default option, it usually isn't required that this is set explicitly; but if you were to do so, it would be set as follows:

config.action_controller.session_store = :cookie_store

Rails provides you with a cookies attribute as well, which enables you to work directly with cookies should you need to. This wraps the process of reading and writing cookies into an abstraction layer that works similarly to a hash.

The CookieStore is a sensible default, given that it's very easy on the server and there is nothing to set up. It is in fact a good choice for applications that expect a lot of traffic. But there are two important caveats: CookieStore is only suitable if the data that's being stored doesn't exceed 4 Kb (it's always a good idea to keep your sessions light) or isn't sensitive/confidential, given that the end user can read the marshaled content.

CookieStore is "easy on the server" because all of the session data is stored client side. All the server has to do is serialize the data and write it in a cookie at the end of the request processing, and conversely read the data from the cookie, verify its integrity, and unmarshal its content for the controller to use.

8.7.3.3. DrbStore

DRb stands for Distributed Ruby and it's a library that's used to share objects via TCP/IP. When adopting DrbStore, the marshaled session data is stored on a DRb server. This server doesn't belong to the application, and as an external entity is not handled by Rails either.

Rails provides you with a DRb server, but it's very primitive and is essentially a hash that's enabled for DRb access. You can find it in C: ubylib ubygems1.8gemsactionpack-2.2.2libaction_controllersessiondrb_server.rb (your actual location may vary depending on where you installed Ruby and whether you installed Rails as a gem).

To use this option, specify:

config.action_controller.session_store = :drb_store

This is theoretically a highly scalable solution for session management, because the distributed nature of DRb would enable scaling out with multiple processes on several servers serving many requests that need to access the same session object. The caveat to this is that an efficient DRb server, with the ability to expire sessions as well, is required (for example, you may have to develop one).

8.7.3.4. MemCacheStore

memcached is a high-performance, distributed memory object caching system that was originally developed for LiveJournal, but now it's used by many of the largest websites on the net, including YouTube, Wikipedia, Slashdot, Flickr, Digg, and Twitter. The basic idea behind this open-source program (released under the BSD license) is to alleviate the database load by reducing the amount of access to the database that's required to retrieve data. Data and objects are in fact cached in memory, often in a distributed fashion across multiple machines. The database is only accessed if the data has not been stored in the in-memory cache. If this is the case, memcached falls back on the database.

This option is set by including the following line in configenvironment.rb (or configenvironmentsproduction.rb if you only need it for production):

config.action_controller.session_store = :mem_cache_store

There are quite a few options that you can specify and the MemCacheStore's configuration and setup are beyond the scope of this book. Just keep in mind that should your high-traffic site adopt memcached to ease the database server load, then taking advantage of memcached for the sessions as well, through the MemCacheStore, is a good idea. As an added benefit, you get the ability to set the expiration time in the configuration, and you no longer have to worry about routinely expiring/cleaning up the session data:

config.action_controller.session_options[:expires] = 1200

8.7.3.5. PStore

When you use this option, ActionController stores sessions within files (in PStore format, keeping the data in marshaled form) on the server. It can be set as follows:

config.action_controller.session_store = :mem_cache_store

Assigning strings to session_options's keys is possible if you want to define a prefix for the files (with :prefix), as well as their location (with :tmpdir). By default the generated files are stored in the tmpsessions folder of your application and can be tidied up as you develop with the rake task tmp:sessions:clear.

In production, the same files will typically need to be cleaned up periodically, often executing the file elimination through cron, which is a scheduling service for Unix-like systems.

In theory PStore can work well if you have a fast SAN (Storage Area Network) in place or if you have one server and a limited amount of traffic; but as the number of sessions grow, beyond a certain threshold having to create so many files in the file system will inevitably become a bottleneck, which will therefore perform poorly when compared to the other solutions mentioned previously.

8.7.3.6. Other Storage Options

For the sake of completeness, two other storage options exist: MemoryStore and FileStore. These store the session data in either the memory (fast but not scalable) or in plain files (which only accept strings and have the same limits of PStore). In practice, no Rails developer worth their salt would be likely to choose either of these options.

If you search for them, you'll also find that other *Store classes have been released by some developers, but these are not part of the Rails core and their quality and stability may vary.

In conclusion, there isn't a clear winner when it comes to choosing how to store your sessions. It depends on too many factors to be able to broadly suggest which one you should take up for your projects. As a very general rule of thumb, I'd say that you might want to start with CookieStore if the 4Kb limit works for you and you don't need to store confidential information in the session. Failing that, ActiveRecordStore is a good choice that's relatively fast, easy to maintain, and scalable. Should the success of your Web application cause you to need to offload some of the database work, then MemCacheStore or DrbStore should do the trick. In any case, I would probably stay away from a file-based solution like PStore.

8.7.3.7. Enabling and Disabling Sessions

Aside from choosing a proper session storage type, you can use the session method within your controllers to fine tune how sessions should be handled. By default sessions are enabled for all the actions within each controller in your application, but it's possible to be more selective than that.

Sessions in Rails 2.3 are "lazy loaded," meaning that they are not loaded unless you make explicit use of them.

To disable sessions within a controller pass :off to the macro-like session method:

class ExampleController < ActionController::Base
  session :off
  #...
end

This disables session management for all the actions defined within ExampleController.

To disable session management for all the actions, save for a few, within a controller, use the :except option:

session :off, :except => %w(download feed)

Conversely, to do the exact opposite and disable sessions for only a few actions, use :only:

session :off, :only => %w(download feed)

An :if option exists for times when you need to disable (or enable) a session based on a condition. This option expects a Proc object:

# Disables session management for all the actions, excluding index,
# if the request was not an HTML or Ajax request.
session :off, :except => :index,
          :if => Proc.new { |req| !(req.format.html? || req.format.js?) }

session can be used within ApplicationController, so that its effects are inherited by all the other controllers. For example, the following disables sessions for the whole application:

class ApplicationController < ActionController::Base
  session :off
  #...
end

If you need to enable sessions for a few controllers and/or a few actions only, you can do so by using session :on. For example, consider the following controllers:

class ApplicationController < ActionController::Base
  session :off
  #...
 end

class FirstController < ActionController::Base
  session :on
  #...
   end

class SecondController < ActionController::Base
  session :on, :only => :feed
  #...
   end

This disables session management for all the controllers, but turns it back on for all the actions in the first controller, and for the feed action in the second controller.

The same method can be used to specify a few more options as well. Some common ones are setting a session domain (:session_domain), a default session id (:session_id), the name to be used for the session in cookies (:session_key), the request path for the cookies (:session_path), and whether sessions are enabled over SSL only (:session_secure).

As a matter of fact, Rails automatically sets the session key based on the application's name. For example, your blog application had the following in configenvironment.rb:

config.action_controller.session = {
  :session_key => '_blog_session',
  :secret      => 'c410dcb43b70ece50b4856a4b2e5d029404338e51d8ae45b642659781...'
}

Remember, session options can be specified within a controller or within an environment configuration file (like Rails 2.x does by default).

For a complete list of accepted options check the documentation for the method ActionController::Base.process_cgi.

8.7.4. Session Expiration

One of the problems related to session management is expiring and/or removing old session data. The reason for this is that you typically don't want users to be able to access session data after a certain amount of time has passed. For example, a login system would normally ask you to re-login after 15, 30, or 60 minutes of inactivity. The need to expire users' sessions is coupled with the fact that sessions stored on the server side take up valuable resources, so over time you need to come up with a system that performs regular clean ups.

The expiration logic varies depending on the session storage option you've chosen, but generally speaking, deleting the session data is easier than, and just as effective as, implementing the expiration of a particular session_id stored in a cookie.

As mentioned before, for PStore sessions, a cron job (or equivalent) that deletes the files within tmpsessions at regular intervals will do. If you are using ActiveRecordStore, you can do the same thing but execute a DELETE SQL query, taking advantage of the updated_at column. For DrbStore, your server should implement the expiration logic for the shared session data. MemCacheStore sessions can be expired automatically by simply configuring the session_options[:expires] option, as shown in the previous section.

Finally, cookie-based sessions are automatically taken care of when the users close their browser, but there are several possible ways to implement a timeout to limit the validity of a cookie to a set amount of time.

You can find a somewhat disorganized discussion about some of these possibilities and available plugins on the official Rails Wiki at http://wiki.rubyonrails.org/rails/pages/HowtoChangeSessionOptions. Some of the information provided is out of date as well, but it's a decent pointer.

Whenever you need to explicitly clear the session data for a current session, you can do so by calling the method reset_session within your actions. This will typically be used for "sign out" or "clear history" types of links in your application, but can also be used when defining your own expiration logic. For example, you could store the cookie time in the session (for example, in session[:updated_at]), and check whether the expiration time has passed. When that's the case, you can invoke reset_session to clear the session data.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.245.233