Expiring Sessions

only for RuBoard - do not distribute or recompile

Server-side state storage is, in general, a better approach than storing state information on the client. However, client-side techniques do have one particular advantage over server-side state management:When your scripts don t store any state on the server, they don t have to be concerned about removing it or expiring it. That becomes the client s problem. Clients handle this in various ways. Passive expiration occurs for state information stored on the client side in URLs or in hidden fields automatically when the user closes the window or goes to a different page. Client-side information stored in a cookie can be more persistent because cookies can be given an expiration date but even so, expiring the cookie is something you get to let the browser worry about.

By contrast, when you manage state data on the server side, you must think about how long you want that information to stick around. Although it s perfectly possible to create longstanding sessions that you intend to allow to exist indefinitely, in other cases, sessions should have a definite lifetime. Sometimes you can take care of this by associating a user action such as Quit or Log Out with session destruction. Unfortunately, there is no guarantee all users will remember to quit or log out, so you may end up with a lot of dead or orphaned sessions in your sessions table. This section explores some techniques for identifying and getting rid of extraneous sessions.

Adding a Timestamp to Session Records

One method for recording the time showing when session records were last modified is to let MySQL do it automatically. To accomplish this, include a TIMESTAMP column in your sessions table:

 CREATE TABLE sessions  (     t           TIMESTAMP,      id          CHAR(32) NOT NULL PRIMARY KEY,      a_session   TEXT  ) 

With this addition, MySQL will update the t column of any session record that is updated. Apache::Session won t do anything with the column (it manages only id and a_session), but you can examine sessions table records yourself. To delete records that haven t been changed in a year, for example, use this query:

 DELETE FROM sessions WHERE t < DATE_SUB(NOW(),INTERVAL 1 YEAR) 

This provides a very simple mechanism for expiring sessions. The drawback is that it assumes you can tell when to delete a session purely on the basis of its last modification date, independent of the content of the session data. That s an invalid assumption under some circumstances, particularly for applications that create state data that may not change frequently. If you store user preferences in a session record, for example, a visitor may set up preferences once and then never change them again. In this case, even if the visitor returns to the site often, the preferences session s modification date doesn t change and the session record expires a year after it was created.

Storing Expiration Values in Session Data

An alternative to using a TIMESTAMP column is to store an expiration time explicitly in the session data under some session attribute name that applications agree to by convention. This approach has the disadvantage that the expiration time is serialized with the other session data, and therefore not directly visible to MySQL. To check a session s expiration date, you must open the session, and then access the appropriate session value. The countervailing advantage is that this approach enables you to write a program that can expire session records without knowing anything about their contents other than the name of the expiration attribute.

If we write an expires() method for setting and getting expiration values, we can hide the details of their representation inside WebDB::Session. Those details are as follows:

  • What name should we use for the key (attribute) associated with the expiration value?

  • How should we represent expiration times?

We know that the key name shouldn t begin with an underscore, because Apache::Session reserves names of that form. But the name also shouldn t be something likely to be used for other purposes by applications that use WebDB::Session. (That means that names such as expiration or expires are probably bad choices.) I ll use the name #<expires># under the assumption that adding non-alphanumeric characters reduces to near zero the probability that an application will use the same name for some other purpose.

Choosing a name gives us an easy way to implement expires() namely, as a call to attr() that supplies the proper attribute name:

 sub expires  { my $self = shift;      return ($self->attr ("#<expires>#", @_));  } 

Then you can set or get an expiration value like this:

 $sess_ref->expires ($expires_time);      # set value  $expires_time = $sess_ref->expires ();   # get value 

The expiration value will be undef if a session has no expiration assigned.

We also must decide how to represent expiration time values. One possibility is to use Perl s time() function, which returns the number of seconds since some reference date (known as the epoch ). Thus, you could do something like this to open a session and assign it an expiration time of three days in the future:

 defined ($sess_ref = WebDB::Session->open (undef, $sess_id))      and $sess_ref->expires (time () + 3*60*60*24); 

Unfortunately, time() values are relative to a reference date, and the reference varies among different platforms. For example, the epoch is typically January 1, 1970 under UNIX and January 1, 1904 under Mac OS. This is a problem if you have Web servers on different platforms that share a common MySQL database to store session records. Scripts that run on the various platforms won t necessarily use the same time reference, with the result that expiration values won t have a consistent baseline.

To solve this problem, we can convert time() values to a non-relative GMT date before storing them, and convert them back to seconds when retrieving them. This allows scripts to use time() but have values stored in the database using a consistent baseline. To implement this mechanism, we can pass time() values to gmtime() to obtain a GMT date as an array containing elements for second, minute, hour, day, month, and year. The inverse transformation is performed by calling timegm() to convert the array back to a time() value. After making these changes, the implementation of expires() is as follows:

 use Time::Local;    # this module is needed because it contains timegm()  sub expires  { my $self = shift;  my $expires;      # If an argument was given, interpret it as a local time() value in      # seconds; convert the value to an absolute GMT date array and store      # a reference to it.      $self->{"#<expires>#"} = [ gmtime ($_[0]) ] if @_;      # Return current expiration value (if there is one) by converting the      # GMT date array back to a local time() value in seconds.  Return undef      # if there is no expiration.      $expires = $self->{"#<expires>#"};      $expires = timegm (@{$expires}) if defined ($expires);      return ($expires);  } 

To hide the fact that we re using time(), we can provide a now() method that scripts can use to get the current time. This helps if you decide later to change the expiration implementation, because you just need to change the expires() and now() methods, not the scripts that call them:

 sub now  {     return (time ());  } 

With these changes, expires() compensates for variations in time reference dates among platforms, and scripts will interoperate properly even if you set the expiration date from a script running on one platform and test it later from a script running on another platform.[5]

[5] Actually, this solution is still subject to some variation between Web server hosts: It assumes those hosts all have the time set correctly, relative to their own time zone.

Now that we have expiration support in place, it s relatively easy to write a little script, expire_session.pl, that scans the sessions table looking for and deleting expired sessions. It fetches the session ID values, and then opens each one and examines its expiration value:

 #! /usr/bin/perl -w  # expire_session.pl - destroy session records that contain an expiration time  # older than current time.  use strict;  use lib qw(/usr/local/apache/lib/perl);  use WebDB;  use WebDB::Session;  my $count = 0;          # number of session records  my $permanent = 0;      # number of records with no expiration value set  my $expired = 0;        # number of records that have expired and were deleted  my $sess_ref;  my $dbh = WebDB::connect ();  my $sth = $dbh->prepare ("SELECT id FROM sessions");  $sth->execute ();  while (my $id = $sth->fetchrow_array ())  {     ++$count;      next unless defined ($sess_ref = WebDB::Session->open ($dbh, $id));      if (!defined ($sess_ref->expires ()))   # session never expires      {         ++$permanent;      }      elsif ($sess_ref->expires () < $sess_ref->now ())      {         $sess_ref->delete ();          undef $sess_ref;          ++$expired;      }      $sess_ref->close () if defined ($sess_ref);  }  $sth->finish ();  $dbh->disconnect ();  print "$count sessions, $permanent permanent, $expired expired\n";  exit (0); 

expire_session.pl does not expire sessions for which expires() returns undef (that is, sessions that have no expiration value). This is important for several reasons:

  • It enables you to create permanent sessions that never expire.

  • It avoids creating a race condition whereby a new session might get clobbered inadvertantly if the expiration process happens to examine the session between the time it is created and the time its expiration value is assigned.

  • It leaves sessions intact that are created by applications that don t follow the convention of assigning an expiration date.

You can run expire_session.pl manually from the command line whenever you want; to run it on a regular basis, install it as a cron job.

Apache::Session Does Not Require Apache

Although the Apache::Session module lives in the Apache namespace, it doesn t actually require Apache and can be used in standalone fashion (for example, by command-line scripts or cron jobs that need access to session records). expire_session.pl is an example of this. It uses WebDB::Session (and therefore Apache::Session), even though you don t run it under Apache.

Let s do something to see session expiration in action. Make a copy of stages_cookie.pl called stages_cookie2.pl. Then change the session-initialization code from this:

 # If this is a new session, initialize the stage counter  $sess_ref->attr ("count", 0) if !defined ($sess_ref->attr ("count")); 

To this:

 # If this is a new session, initialize the stage counter and the expiration.  $sess_ref->attr ("count", 0) if !defined ($sess_ref->attr ("count"));  $sess_ref->expires ($sess_ref->now () + 60) if !defined ($sess_ref->expires ());  # Display when expiration will occur  $page .= p (sprintf ("Expiration occurs in %d seconds.",                          $sess_ref->expires() - $sess_ref->now ())); 

The new code adds a session expiration of 60 seconds and prints the number of seconds until the session expires. Invoke stages_cookie2.pl and click the continue link several times. You ll notice the expiration value decreasing each time. Finally, it reaches zero and then goes negative! Hmmm. Shouldn t the session expire? Yes, but remember we re handling removal of expired sessions externally using expire_session.pl. Run that program manually and it should remove your session. Then select the continue link in your browser window again. stages_cookie2.pl should discover your session to be missing and generate a new one with a fresh 60-second expiration.

We could modify the WebDB::Session module s open() method to check a session when you open it and delete it automatically if its expiration time has been reached. However, that would enforce a constraint making it impossible for an application to open expired sessions, should it want to do so for some reason (for example, to gather statistics on the expiration dates of records in the sessions table). Another approach that provides some flexibility to applications is to leave open() unchanged and write another method, open_with_expiration(), that is like it but performs the expiration check. This method can be implemented rather simply as a call to open() followed by a test of the expiration value:[6]

[6] open_with_expiration() calls &open() rather than open() due to an ambiguity with Perl s built-in function of the same name. Adding & resolves the name to the version in our module file.

 sub open_with_expiration  { my $self = &open (@_);      # If session exists and has an expiration time that      # has passed, clobber it.      if (defined ($self) && defined ($self->expires ())          && $self->expires () < $self->now ())      {         $WebDB::Session::errstr = sprintf ("Session %s has expired",                                                  $self->session_id ());          $self->delete ();          $self = undef;      }      return ($self);  } 

For any session that has not expired, open_with_expiration() is functionally equivalent to open(). Otherwise, it deletes the session and returns undef.

Now modify stages_cookie2.pl to use the new method. The line that tries to open an existing session looks like this:

 $sess_ref = WebDB::Session->open (undef, $sess_id) if defined ($sess_id); 

Change it to invoke open_with_expiration() instead:

 $sess_ref = WebDB::Session->open_with_expiration (undef, $sess_id)                                                  if defined ($sess_id); 

When you try the modified stages_cookie2.pl and select the continue link repeatedly, you should see that as the current session reaches its expiration time, it gets deleted automatically and a new session begins.

Expiring sessions this way doesn t eliminate the need for an external program that scans the sessions table periodically, because automatic expiration takes place only for sessions that you open by calling open_with_expiration(). On the other hand, an expiration scan becomes a bit simpler, because you can let WebDB::Session do the actual work of deleting the sessions. All you need to do is sweep through the sessions table, opening each session and closing those for which open_with_expiration() doesn t return undef. Here s how you d write the loop for a script, expire_session2.pl, that is similar to expire_session.pl, but uses the new session-opening method:

 while (my $id = $sth->fetchrow_array ())  {     ++$count;      $sess_ref = WebDB::Session->open_with_expiration ($dbh, $id);      if (!defined ($sess_ref))      {         ++$expired;     # session has expired          next;      }      # session never expires if there's no expiration value      ++$permanent if !defined ($sess_ref->expires ());      $sess_ref->close ();  } 

One problem with running an expiration scan is that it locks the table while you re reading the set of session ID values. Then it locks the table again while reading each session record, and again if you delete the record. That s a lot of locking, and it may have a negative impact on your site s performance if you have heavy session activity going on. This problem will be greatly alleviated when MySQL gains the capability to perform row-level locking rather than table locking (a capability that is under development as I am writing this).

In the meantime, to alleviate locking problems, you could incorporate other techniques into the table scan such as putting a delay between session reads or splitting the scan into segments. Slowing a script down deliberately isn t something you do with very many scripts, because normally you want your scripts to execute quickly. However, an expiration scan has a much lower priority than servicing page requests, so preventing it from interfering with page requests is more important than having it run fast. And it s very easy to implement a delay: Just put a sleep() call into the session-checking loop. The following statement at the beginning or end of the loop puts a one-second delay between the processing for each session:

 sleep (1); 

Make sure you don t issue the sleep() call while the session is open, however. You want the script to hold sessions open for as brief a time as possible.

If your sessions table is large, splitting a scan into segments is one way to keep MySQL from returning a huge session ID result set, so that you can process the table in smaller, more manageable pieces. The following is the main part of a script, expire_session3.pl, that segments the scan into 100-record pieces using MySQL s LIMIT clause:

 $chunk_size = 100;  $count = $dbh->selectrow_array ("SELECT COUNT(*) FROM sessions");  $offset = $count;  while ($offset > 0)  {     $offset -= $chunk_size;      if ($offset < 0) # final chunk is only partial      {         $chunk_size += $offset;          $offset = 0;      }      $offset = 0 if $offset < 0;      my $sth = $dbh->prepare (                 "SELECT id FROM sessions LIMIT $offset, $chunk_size");      $sth->execute ();      my $sess_ref;      while (my $id = $sth->fetchrow_array ())      {         $sess_ref = WebDB::Session->open_with_expiration ($dbh, $id);          if (!defined ($sess_ref))          {             ++$expired; # session has expired              next;          }          # session never expires if there's no expiration value          ++$permanent if !defined ($sess_ref->expires ());          $sess_ref->close ();      }      $sth->finish ();  } 

The loop reads sections of the table from the end to the beginning to avoid a problem that occurs if you read from the beginning to the end. Suppose you read the first 100 session records and expire 50 of them. That causes 50 of the following records in the table to shift down into the first 100 records, so that when you read the next 100 records, you miss those 50. (Of course, any kind of scan that reads a table in sections may be affected by record-shifting when there are other programs modifying the table, such as scripts that create new sessions. There s not much we can do about that, but we may as well at least keep the script from interfering with itself!)

Adding an Expiration Column to Session Records

Another way to perform expiration, which I ll just outline rather than showing an implementation, would be to add an expires column to the sessions table, and then modify WebDB::Session to store expiration values in that column rather than in the session data. This change can be implemented inside the expires() method such that it wouldn t be visible to applications that use WebDB::Session.

Storing expiration values this way would require issuing queries to MySQL (because Apache::Session won t manipulate the expires column for you). However, putting values in a separate column would make it much easier for the expiration scan to do its job. It wouldn t need to open sessions to find out expiration values; it could examine the expires column directly.

Strategies for Using Expiration Dates

When you associate each session record with the appropriate client by means of a cookie that contains the session identifier, you can use several expiration strategies:

  • Assign an expiration to a session and the cookie only when the session is first opened. This causes the session and the cookie to have a fixed lifetime. They both expire when the given date is reached, regardless of whether and how often the user visits your site in the meantime.

  • Assign an expiration to a session and the cookie each time the session is opened. This extends the expiration date each time the user visits. As long as the visits occur at intervals shorter than the expiration period, the session and the cookie both remain active. Under this strategy, your applicatin must resend the cookie to the browser in the response to each request, not just when the session is created. Otherwise, the browser won t know the cookie lifetime has been extended.

  • Assign no expiration at all. In this case, the session and the cookie behave quite differently. The session doesn t expire at all, but the cookie expires (is deleted by the browser) when the browser exits. If you want a permanent session, you need the cookie to have a long lifetime as well. Assign it an explicit expiration date as far as possible in the future.

only for RuBoard - do not distribute or recompile


MySQL and Perl for the Web
MySQL and Perl for the Web
ISBN: 0735710546
EAN: 2147483647
Year: 2005
Pages: 77
Authors: Paul DuBois

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net