? includes/handlers.inc
Index: includes/bootstrap.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v
retrieving revision 1.288
diff -u -p -r1.288 bootstrap.inc
--- includes/bootstrap.inc	1 Jul 2009 12:47:30 -0000	1.288
+++ includes/bootstrap.inc	12 Jul 2009 21:24:01 -0000
@@ -1405,6 +1405,9 @@ function _drupal_bootstrap($phase) {
       }
 
       // Prepare for non-cached page workflow.
+      require_once DRUPAL_ROOT . '/' . variable_get('lock_inc', 'includes/lock.inc');
+      lock_init();
+
       if ($_SERVER['SERVER_SOFTWARE'] !== 'PHP CLI') {
         ob_start();
         drupal_page_header();
Index: includes/lock.inc
===================================================================
RCS file: includes/lock.inc
diff -N includes/lock.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/lock.inc	12 Jul 2009 21:24:01 -0000
@@ -0,0 +1,199 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * A database-mediated implementation of a locking mechanism.
+ */
+
+/**
+ * @defgroup locks Lock acquire and release functions
+ * @{
+ * Functions to coordinate long-running operations across requests.
+ *
+ * This is a cooperative, advisory lock system. Any long-running
+ * operation which could potentially be attempted in parallel by multiple
+ * requests should try to acquire a lock before proceeding.  For example,
+ * functions doing a large number of database inserts after building new
+ * data based on a drupal variable's state.  If a function fails to acquire
+ * a lock it should either immediately return, or call lock_wait() if the
+ * rest of the page request requires that the operation in question be
+ * complete.  After lock_wait() returns, the function may again attempt to
+ * acquire a lock, or may simply allow the page request to proceed on the
+ * assumption that a parallel request completed the operation.
+ *
+ * lock_acquire() and lock_wait() will break stale locks, so that they do
+ * not persist.
+ *
+ * A function that has acquired a lock may attempt to renew a lock (extend the
+ * duration of the lock) by calling lock_acquire() again during the operation.
+ * lock. Failure to renew a lock is indicative that another request has
+ * acquired the lock, and that the current operation may need to be aborted.
+ *
+ * Alternative implementations of this API (such as APC) may be substituted
+ * by setting the 'lock_inc' variable to an alternate include filepath.
+ */
+
+/**
+ * Initialize the locking system.
+ */
+function lock_init() {
+  global $locks;
+
+  $locks = array();
+}
+
+/**
+ * Helper function to get this request's unique id.
+ */
+function _lock_id() {
+  $lock_id = &drupal_static(__FUNCTION__);
+
+  if (!isset($lock_id)) {
+    // Assign a unique id.
+    $lock_id = uniqid(mt_rand(), TRUE);
+    // We only register a shutdown function if a lock is used.
+    register_shutdown_function('lock_release_all', $lock_id);
+  }
+  return $lock_id;
+}
+
+/**
+ * Acquire (or renew) a lock, but do not block if it fails.
+ *
+ * @param $name
+ *   The name of the lock.
+ * @param $timeout
+ *   A number of seconds (float) before the lock expires.
+ * @return TRUE if the lock was acquired, FALSE if it failed.
+ */
+function lock_acquire($name, $timeout = 30.0) {
+  global $locks;
+
+  if (isset($locks[$name])) {
+    // We acquired a lock before, so try to renew it.
+    $expire = microtime(TRUE) + $timeout;
+    $success = (bool)db_update('semaphore')
+      ->fields(array('expire' => $expire))
+      ->condition('name', $name)
+      ->condition('value', _lock_id())
+      ->execute();
+    if (!$success) {
+      // The lock was broken.
+      unset($locks[$name]);
+    }
+    return $success;
+  }
+  else {
+    // Optimistically try to acquire the lock, then
+    // retry once if it fails.
+    $expire = microtime(TRUE) + $timeout;
+    $retry = FALSE;
+    do {
+      try {
+        db_insert('semaphore')
+          ->fields(array(
+            'name' => $name,
+            'value' => _lock_id(),
+            'expire' => $expire,
+          ))
+          ->execute();
+        $locks[$name] = TRUE;
+        $retry = FALSE;
+      }
+      catch (PDOException $e) {
+        // Suppress the error, and if this is our first pass through the loop,
+        // try to break the lock in case it's expired.
+        if (!$retry) {
+          $retry = lock_may_be_available($name);
+        }
+      }
+    } while ($retry);
+  }
+  return isset($locks[$name]);
+}
+
+/**
+ * Check if lock acquired by a different process may be available.
+ *
+ * If an existing lock has expired, it is removed.
+ *
+ * @param $name
+ *   The name of the lock.
+ *
+ * @return TRUE if there is no lock or it was removed, FALSE otherwise.
+ */
+function lock_may_be_available($name) {
+  $lock = db_query("SELECT expire, value FROM {semaphore} WHERE name = :name", array(':name' => $name))->fetchAssoc();
+  if (!$lock) {
+    return TRUE;
+  }
+  $now = microtime(TRUE);
+  if ($now > $lock['expire']) {
+    // We check two conditions to prevent a race condition where another
+    // thread acquired the lock and set a new expire time.
+    return (bool)db_delete('semaphore')
+      ->condition('name', $name)
+      ->condition('value', $lock['value'])
+      ->condition('expire', $lock['expire'])
+      ->execute();
+  }
+  return FALSE;
+}
+
+/**
+ * Wait for a lock to be available.
+ *
+ * This will block until the lock is available, so should not be used with
+ * locks that are acquired very frequently.
+ *
+ * @param $name
+ *   The name of the lock.
+ * @param $delay
+ *   The maximum number of seconds to wait.
+ * @return TRUE if the lock holds, FALSE if it is available.
+ */
+function lock_wait($name, $delay = 30) {
+  while ($delay--) {
+    sleep(1);
+    if (lock_may_be_available($name)) {
+      return FALSE;
+    }
+  }
+  return TRUE;
+}
+
+/**
+ * Release a lock previously acquired by lock_acquire().
+ *
+ * @param $name
+ *   The name of the lock.
+ */
+function lock_release($name) {
+  global $locks;
+
+  unset($locks[$name]);
+  db_delete('semaphore')
+    ->condition('name', $name)
+    ->condition('value', _lock_id())
+    ->execute();
+}
+
+/**
+ * Release all previously acquired locks.
+ */
+function lock_release_all($lock_id = NULL) {
+  global $locks;
+
+  $locks = array();
+  if (empty($lock_id)) {
+    $lock_id = _lock_id();
+  }
+  db_delete('semaphore')
+    ->condition('value', $lock_id)
+    ->execute();
+}
+
+/**
+ * @} End of "defgroup locks".
+ */
Index: includes/menu.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/menu.inc,v
retrieving revision 1.329
diff -u -p -r1.329 menu.inc
--- includes/menu.inc	9 Jul 2009 07:03:57 -0000	1.329
+++ includes/menu.inc	12 Jul 2009 21:24:01 -0000
@@ -1837,18 +1837,35 @@ function menu_cache_clear_all() {
  * schedule a call to itself on the first real page load from
  * menu_execute_active_handler(), because the maintenance page environment
  * is different and leaves stale data in the menu tables.
+ *
+ * @return
+ *  TRUE if the menu was rebuilt, FALSE if another thread was rebuilding
+ *  in parallel and the current thread just waited for completion.
  */
 function menu_rebuild() {
-  variable_del('menu_rebuild_needed');
+  if (!lock_acquire('menu_rebuild')) {
+    // Wait for another request that is already doing this work.
+    // We choose to block here since otherwise the router item may not 
+    // be avaiable in menu_execute_active_handler() resulting in a 404.
+    lock_wait('menu_rebuild');
+    return FALSE;
+  }
+
   list($menu, $masks) = menu_router_build();
   _menu_router_save($menu, $masks);
   _menu_navigation_links_rebuild($menu);
+  // Clear the menu, page and block caches.
   menu_cache_clear_all();
-  // Clear the page and block caches.
   _menu_clear_page_cache();
+
   if (defined('MAINTENANCE_MODE')) {
     variable_set('menu_rebuild_needed', TRUE);
   }
+  else {
+    variable_del('menu_rebuild_needed');
+  }
+  lock_release('menu_rebuild');
+  return TRUE;
 }
 
 /**
Index: modules/locale/locale.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/locale/locale.module,v
retrieving revision 1.243
diff -u -p -r1.243 locale.module
--- modules/locale/locale.module	5 Jul 2009 18:00:09 -0000	1.243
+++ modules/locale/locale.module	12 Jul 2009 21:24:01 -0000
@@ -375,7 +375,7 @@ function locale($string = NULL, $context
       if ($cache = cache_get('locale:' . $langcode, 'cache')) {
         $locale_t[$langcode] = $cache->data;
       }
-      else {
+      elseif (lock_acquire('locale_cache_' . $langcode)) {
         // Refresh database stored cache of translations for given language.
         // We only store short strings used in current version, to improve
         // performance and consume less memory.
@@ -384,6 +384,7 @@ function locale($string = NULL, $context
           $locale_t[$langcode][$data->context][$data->source] = (empty($data->translation) ? TRUE : $data->translation);
         }
         cache_set('locale:' . $langcode, $locale_t[$langcode]);
+        lock_release('locale_cache_' . $langcode);
       }
     }
   }
Index: modules/system/system.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.install,v
retrieving revision 1.354
diff -u -p -r1.354 system.install
--- modules/system/system.install	10 Jul 2009 04:28:15 -0000	1.354
+++ modules/system/system.install	12 Jul 2009 21:24:01 -0000
@@ -1288,6 +1288,31 @@ function system_schema() {
     'primary key' => array('filename'),
   );
 
+  $schema['semaphore'] = array(
+    'description' => 'Table for storing locks and other flags that should not be cached by the variable system.',
+    'fields' => array(
+      'name' => array(
+        'description' => 'Primary Key: Unique name.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => ''),
+      'value' => array(
+        'description' => 'A value for the semaphore.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => ''),
+      'expire' => array(
+        'description' => 'A Unix timestamp with microseconds indicating when the semaphore should expire.',
+        'type' => 'float',
+        'size' => 'big',
+        'not null' => TRUE),
+      ),
+    'indexes' => array('value' => array('value')),
+    'primary key' => array('name'),
+    );
+
   $schema['sessions'] = array(
     'description' => "Drupal's session handlers read and write into the sessions table. Each record represents a user session, either anonymous or authenticated.",
     'fields' => array(
