Index: includes/session.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/session.inc,v
retrieving revision 1.79
diff -u -9 -p -r1.79 session.inc
--- includes/session.inc	9 Mar 2010 03:52:02 -0000	1.79
+++ includes/session.inc	16 Mar 2010 22:18:29 -0000
@@ -55,24 +55,33 @@ function _drupal_session_close() {
  * This function will be called by PHP to retrieve the current user's
  * session data, which is stored in the database. It also loads the
  * current user's appropriate roles into the user object.
  *
  * This function should not be called directly. Session data should
  * instead be accessed via the $_SESSION superglobal.
  *
  * @param $sid
  *   Session ID.
+ * @param $return_last
+ *   If TRUE, return the same value as last time this function was called, if
+ *   the session ID in the two calls are identical. This argument is not used
+ *   when called by PHP.
  * @return
  *   Either an array of the session data, or an empty string, if no data
  *   was found or the user is anonymous.
  */
-function _drupal_session_read($sid) {
+function _drupal_session_read($sid, $return_last = FALSE) {
   global $user, $is_https;
+  static $last_user;
+
+  if ($return_last) {
+    return !empty($last_user->sid) && $last_user->sid == $sid ? $last_user->session : NULL;
+  }
 
   // Write and Close handlers are called after destructing objects
   // since PHP 5.0.5.
   // Thus destructors can use sessions but session handler can't use objects.
   // So we are moving session closure before destructing objects.
   drupal_register_shutdown_function('session_write_close');
 
   // Handle the case of first time visitors and clients that don't store
   // cookies (eg. web crawlers).
@@ -112,18 +121,20 @@ function _drupal_session_read($sid) {
     $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.
   else {
     $session = isset($user->session) ? $user->session : '';
     $user = drupal_anonymous_user($session);
   }
 
+  $last_user = $user;
+
   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.
  *
@@ -139,43 +150,56 @@ function _drupal_session_read($sid) {
  */
 function _drupal_session_write($sid, $value) {
   global $user, $is_https;
 
   if (!drupal_save_session()) {
     // We don't have anything to do if we are not allowed to save the session.
     return;
   }
 
-  $fields = array(
-    'uid' => $user->uid,
-    'cache' => isset($user->cache) ? $user->cache : 0,
-    'hostname' => ip_address(),
-    'session' => $value,
-    'timestamp' => REQUEST_TIME,
-  );
-  $key = array('sid' => $sid);
-  if ($is_https) {
-    $key['ssid'] = $sid;
-    $insecure_session_name = substr(session_name(), 1);
-    // The "secure pages" setting allows a site to simultaneously use both
-    // secure and insecure session cookies. If enabled, use the insecure session
-    // identifier as the sid.
-    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.
+  $is_changed = _drupal_session_read($sid, TRUE) !== $value;
+
+  if ($is_changed) {
+    $fields = array(
+      'uid' => $user->uid,
+      'cache' => isset($user->cache) ? $user->cache : 0,
+      'hostname' => ip_address(),
+      'session' => $value,
+      'timestamp' => REQUEST_TIME,
+    );
+    $key = array('sid' => $sid);
+    if ($is_https) {
+      $key['ssid'] = $sid;
+      $insecure_session_name = substr(session_name(), 1);
+      // The "secure pages" setting allows a site to simultaneously use both
+      // secure and insecure session cookies. If enabled, use the insecure session
+      // identifier as the sid.
+      if (variable_get('https', FALSE) && isset($_COOKIE[$insecure_session_name])) {
+        $key['sid'] = $_COOKIE[$insecure_session_name];
+      }
     }
+    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.
+  // For performance reasons, last access time and session timestamp are updated
+  // no more frequently than once every 180 seconds.
+  if (!$is_changed && REQUEST_TIME - $user->timestamp > variable_get('session_write_interval', 180)) {
+    db_update('sessions')
+      ->fields(array(
+        'timestamp' => REQUEST_TIME
+      ))
+      ->condition('sid', $sid)
+      ->execute();
+  }
   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();
   }
 
Index: modules/simpletest/tests/session.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/session.test,v
retrieving revision 1.27
diff -u -9 -p -r1.27 session.test
--- modules/simpletest/tests/session.test	17 Feb 2010 08:56:48 -0000	1.27
+++ modules/simpletest/tests/session.test	16 Mar 2010 22:18:29 -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;
 
