Index: includes/bootstrap.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v
retrieving revision 1.419
diff -u -9 -p -r1.419 bootstrap.inc
--- includes/bootstrap.inc	16 Sep 2010 01:10:38 -0000	1.419
+++ includes/bootstrap.inc	20 Sep 2010 15:38:01 -0000
@@ -1905,25 +1905,24 @@ function drupal_hash_base64($data) {
   // Modify the hash so it's safe to use in URLs.
   return strtr($hash, array('+' => '-', '/' => '_', '=' => ''));
 }
 
 /**
  * Generates a default anonymous $user object.
  *
  * @return Object - the user object.
  */
-function drupal_anonymous_user($session = '') {
+function drupal_anonymous_user() {
   $user = new stdClass();
   $user->uid = 0;
   $user->hostname = ip_address();
   $user->roles = array();
   $user->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user';
-  $user->session = $session;
   $user->cache = 0;
   return $user;
 }
 
 /**
  * A string describing a phase of Drupal to load. Each phase adds to the
  * previous one, so invoking a later phase automatically runs the earlier
  * phases too. The most important usage is that if you want to access the
  * Drupal database from a script without loading anything else, you can
Index: includes/session.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/session.inc,v
retrieving revision 1.89
diff -u -9 -p -r1.89 session.inc
--- includes/session.inc	17 Sep 2010 14:56:27 -0000	1.89
+++ includes/session.inc	20 Sep 2010 15:38:01 -0000
@@ -106,25 +106,39 @@ function _drupal_session_read($sid) {
   if ($user && $user->uid > 0 && $user->status == 1) {
     // This is done to unserialize the data member of $user.
     $user->data = unserialize($user->data);
 
     // Add roles element to $user.
     $user->roles = array();
     $user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
     $user->roles += db_query("SELECT r.rid, r.name FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid = :uid", array(':uid' => $user->uid))->fetchAllKeyed(0, 1);
   }
-  // We didn't find the client's record (session has expired), or they are
-  // blocked, or they are an anonymous user.
+  elseif ($user) {
+    // The user is anonymous or blocked. Only preserve two fields from the
+    // {sessions} table.
+    $account = drupal_anonymous_user();
+    $account->session = $user->session;
+    $account->timestamp = $user->timestamp;
+    $user = $account;
+  }
   else {
-    $session = isset($user->session) ? $user->session : '';
-    $user = drupal_anonymous_user($session);
+    // The session has expired.
+    $user = drupal_anonymous_user();
+    $user->session = '';
   }
 
+  // Store the session that was read for comparison in _drupal_session_write().
+  $last_read = &drupal_static('drupal_session_last_read');
+  $last_read = array(
+    'sid' => $sid,
+    'value' => $user->session,
+  );
+
   return $user->session;
 }
 
 /**
  * Session handler assigned by session_set_save_handler().
  *
  * This function will be called by PHP to store the current user's
  * session, which Drupal saves to the database.
  *
@@ -136,80 +150,90 @@ function _drupal_session_read($sid) {
  * @param $value
  *   Serialized array of the session data.
  *
  * @return
  *   This function will always return TRUE.
  */
 function _drupal_session_write($sid, $value) {
   global $user, $is_https;
 
-  // The exception handler is not active at this point, so we need to do it manually.
+  // The exception handler is not active at this point, so we need to do it
+  // manually.
   try {
     if (!drupal_save_session()) {
       // We don't have anything to do if we are not allowed to save the session.
       return;
     }
 
-    // Either ssid or sid or both will be added from $key below.
-    $fields = array(
-      'uid' => $user->uid,
-      'cache' => isset($user->cache) ? $user->cache : 0,
-      'hostname' => ip_address(),
-      'session' => $value,
-      'timestamp' => REQUEST_TIME,
-    );
-
-    // The "secure pages" setting allows a site to simultaneously use both secure
-    // and insecure session cookies. If enabled and both cookies are presented
-    // then use both keys. If not enabled but on HTTPS then use the PHP session
-    // id as 'ssid'. If on HTTP then use the PHP session id as 'sid'.
-    if ($is_https) {
-      $key['ssid'] = $sid;
-      $insecure_session_name = substr(session_name(), 1);
-      if (variable_get('https', FALSE) && isset($_COOKIE[$insecure_session_name])) {
-        $key['sid'] = $_COOKIE[$insecure_session_name];
+    // Check whether $_SESSION has been changed in this request.
+    $last_read = &drupal_static('drupal_session_last_read');
+    $is_changed = !isset($last_read) || $last_read['sid'] != $sid || $last_read['value'] !== $value;
+
+    // For performance reasons, do not update the sessions table, unless
+    // $_SESSION has changed or more than 180 has passed since the last update.
+    if ($is_changed || REQUEST_TIME - $user->timestamp > variable_get('session_write_interval', 180)) {
+      // Either ssid or sid or both will be added from $key below.
+      $fields = array(
+        'uid' => $user->uid,
+        'cache' => isset($user->cache) ? $user->cache : 0,
+        'hostname' => ip_address(),
+        'session' => $value,
+        'timestamp' => REQUEST_TIME,
+      );
+
+      // The "secure pages" setting allows a site to simultaneously use both
+      // secure and insecure session cookies. If enabled and both cookies are
+      // presented then use both keys. If not enabled but on HTTPS then use the
+      // PHP session id as 'ssid'. If on HTTP then use the PHP session id as
+      // 'sid'.
+      if ($is_https) {
+        $key['ssid'] = $sid;
+        $insecure_session_name = substr(session_name(), 1);
+        if (variable_get('https', FALSE) && isset($_COOKIE[$insecure_session_name])) {
+          $key['sid'] = $_COOKIE[$insecure_session_name];
+        }
+      }
+      else {
+        $key['sid'] = $sid;
       }
-    }
-    else {
-      $key['sid'] = $sid;
-    }
 
-    db_merge('sessions')
-      ->key($key)
-      ->fields($fields)
-      ->execute();
+      db_merge('sessions')
+        ->key($key)
+        ->fields($fields)
+        ->execute();
+    }
 
-    // Last access time is updated no more frequently than once every 180 seconds.
-    // This reduces contention in the users table.
+    // Likewise, do not update access time more than once per 180 seconds.
     if ($user->uid && REQUEST_TIME - $user->access > variable_get('session_write_interval', 180)) {
       db_update('users')
         ->fields(array(
           'access' => REQUEST_TIME
         ))
         ->condition('uid', $user->uid)
         ->execute();
     }
 
     return TRUE;
   }
   catch (Exception $exception) {
     require_once DRUPAL_ROOT . '/includes/errors.inc';
-    // If we are displaying errors, then do so with no possibility of a further uncaught exception being thrown.
+    // If we are displaying errors, then do so with no possibility of a further
+    // uncaught exception being thrown.
     if (error_displayable()) {
       print '<h1>Uncaught exception thrown in session handler.</h1>';
       print '<p>' . _drupal_render_exception_safe($exception) . '</p><hr />';
     }
     return FALSE;
   }
 }
 
 /**
- * Initialize the session handler, starting a session if needed.
+ * Initializes the session handler, starting a session if needed.
  */
 function drupal_session_initialize() {
   global $user, $is_https;
 
   session_set_save_handler('_drupal_session_open', '_drupal_session_close', '_drupal_session_read', '_drupal_session_write', '_drupal_session_destroy', '_drupal_session_garbage_collection');
 
   if (isset($_COOKIE[session_name()]) || ($is_https && variable_get('https', FALSE) && isset($_COOKIE[substr(session_name(), 1)]))) {
     // If a session cookie exists, initialize the session. Otherwise the
     // session is only started on demand in drupal_session_commit(), making
@@ -229,19 +253,19 @@ function drupal_session_initialize() {
     // Less random sessions (which are much faster to generate) are used for
     // anonymous users than are generated in drupal_session_regenerate() when
     // a user becomes authenticated.
     session_id(drupal_hash_base64(uniqid(mt_rand(), TRUE)));
   }
   date_default_timezone_set(drupal_get_user_timezone());
 }
 
 /**
- * Forcefully start a session, preserving already set session data.
+ * Forcefully starts a session, preserving already set session data.
  *
  * @ingroup php_wrappers
  */
 function drupal_session_start() {
   // Command line clients do not support cookies nor sessions.
   if (!drupal_session_started() && !drupal_is_cli()) {
     // Save current session data before starting it, as PHP will destroy it.
     $session_data = isset($_SESSION) ? $_SESSION : NULL;
 
@@ -250,19 +274,19 @@ function drupal_session_start() {
 
     // Restore session data.
     if (!empty($session_data)) {
       $_SESSION += $session_data;
     }
   }
 }
 
 /**
- * Commit the current session, if necessary.
+ * Commits the current session, if necessary.
  *
  * If an anonymous user already have an empty session, destroy it.
  */
 function drupal_session_commit() {
   global $user;
 
   if (!drupal_save_session()) {
     // We don't have anything to do if we are not allowed to save the session.
     return;
@@ -281,19 +305,19 @@ function drupal_session_commit() {
     if (!drupal_session_started()) {
       drupal_session_start();
     }
     // Write the session data.
     session_write_close();
   }
 }
 
 /**
- * Return whether a session has been started.
+ * Returns whether a session has been started.
  */
 function drupal_session_started($set = NULL) {
   static $session_started = FALSE;
   if (isset($set)) {
     $session_started = $set;
   }
   return $session_started && session_id();
 }
 
@@ -359,19 +383,19 @@ function drupal_session_regenerate() {
     drupal_session_start();
     $user = $account;
   }
   date_default_timezone_set(drupal_get_user_timezone());
 }
 
 /**
  * Session handler assigned by session_set_save_handler().
  *
- * Cleanup a specific session.
+ * Cleans up a specific session.
  *
  * @param $sid
  *   Session ID.
  */
 function _drupal_session_destroy($sid) {
   global $user, $is_https;
 
   // Delete session data.
   db_delete('sessions')
@@ -401,52 +425,52 @@ function _drupal_session_destroy($sid) {
 function _drupal_session_delete_cookie($name, $force_insecure = FALSE) {
   if (isset($_COOKIE[$name])) {
     $params = session_get_cookie_params();
     setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], !$force_insecure && $params['secure'], $params['httponly']);
     unset($_COOKIE[$name]);
   }
 }
 
 /**
- * End a specific user's session(s).
+ * Ends a specific user's session(s).
  *
  * @param $uid
  *   User ID.
  */
 function drupal_session_destroy_uid($uid) {
   db_delete('sessions')
     ->condition('uid', $uid)
     ->execute();
 }
 
 /**
  * Session handler assigned by session_set_save_handler().
  *
- * Cleanup stalled sessions.
+ * Cleans up stalled sessions.
  *
  * @param $lifetime
  *   The value of session.gc_maxlifetime, passed by PHP.
  *   Sessions not updated for more than $lifetime seconds will be removed.
  */
 function _drupal_session_garbage_collection($lifetime) {
   // Be sure to adjust 'php_value session.gc_maxlifetime' to a large enough
   // value. For example, if you want user sessions to stay in your database
   // for three weeks before deleting them, you need to set gc_maxlifetime
   // to '1814400'. At that value, only after a user doesn't log in after
   // three weeks (1814400 seconds) will his/her session be removed.
   db_delete('sessions')
     ->condition('timestamp', REQUEST_TIME - $lifetime, '<')
     ->execute();
   return TRUE;
 }
 
 /**
- * Determine whether to save session data of the current request.
+ * Determines whether to save session data of the current request.
  *
  * This function allows the caller to temporarily disable writing of
  * session data, should the request end while performing potentially
  * dangerous operations, such as manipulating the global $user object.
  * See http://drupal.org/node/218104 for usage.
  *
  * @param $status
  *   Disables writing of session data when FALSE, (re-)enables
  *   writing when TRUE.
Index: modules/simpletest/tests/session.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/session.test,v
retrieving revision 1.32
diff -u -9 -p -r1.32 session.test
--- modules/simpletest/tests/session.test	1 Sep 2010 20:08:17 -0000	1.32
+++ modules/simpletest/tests/session.test	20 Sep 2010 15:38:02 -0000
@@ -175,18 +175,60 @@ class SessionTestCase extends DrupalWebT
 
     // Verify that no message is displayed.
     $this->drupalGet('');
     $this->assertSessionCookie(FALSE);
     $this->assertSessionEmpty(TRUE);
     $this->assertNoText(t('This is a dummy message.'), t('The message was not saved.'));
   }
 
   /**
+   * Test that sessions are only saved when necessary.
+   */
+  function testSessionWrite() {
+    $user = $this->drupalCreateUser(array('access content'));
+    $this->drupalLogin($user);
+
+    $sql = 'SELECT u.access, s.timestamp FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE u.uid = :uid';
+    $times1 = db_query($sql, array(':uid' => $user->uid))->fetchObject();
+
+    // Before every request we sleep one second to make sure that if the session
+    // is saved, its timestamp will change.
+
+    // Modify the session.
+    sleep(1);
+    $this->drupalGet('session-test/set/foo');
+    $times2 = db_query($sql, array(':uid' => $user->uid))->fetchObject();
+    $this->assertEqual($times2->access, $times1->access, t('Users table was not updated.'));
+    $this->assertNotEqual($times2->timestamp, $times1->timestamp, t('Sessions table was updated.'));
+
+    // Write the same value again, i.e. do not modify the session.
+    sleep(1);
+    $this->drupalGet('session-test/set/foo');
+    $times3 = db_query($sql, array(':uid' => $user->uid))->fetchObject();
+    $this->assertEqual($times3->access, $times1->access, t('Users table was not updated.'));
+    $this->assertEqual($times3->timestamp, $times2->timestamp, t('Sessions table was not updated.'));
+
+    // Do not change the session.
+    sleep(1);
+    $this->drupalGet('');
+    $times4 = db_query($sql, array(':uid' => $user->uid))->fetchObject();
+    $this->assertEqual($times4->access, $times3->access, t('Users table was not updated.'));
+    $this->assertEqual($times4->timestamp, $times3->timestamp, t('Sessions table was not updated.'));
+
+    // Force updating of users and sessions table once per second.
+    variable_set('session_write_interval', 0);
+    $this->drupalGet('');
+    $times5 = db_query($sql, array(':uid' => $user->uid))->fetchObject();
+    $this->assertNotEqual($times5->access, $times4->access, t('Users table was updated.'));
+    $this->assertNotEqual($times5->timestamp, $times4->timestamp, t('Sessions table was updated.'));
+  }
+
+  /**
    * Reset the cookie file so that it refers to the specified user.
    *
    * @param $uid User id to set as the active session.
    */
   function sessionReset($uid = 0) {
     // Close the internal browser.
     $this->curlClose();
     $this->loggedInUser = FALSE;
 
