diff --git a/.htaccess b/.htaccess index 9494b53..95151dc 100644 --- a/.htaccess +++ b/.htaccess @@ -102,6 +102,7 @@ DirectoryIndex index.php index.html index.htm # If your site is running in a VirtualDocumentRoot at http://example.com/, # uncomment the following line: # RewriteBase / + RewriteBase / # Pass all requests not referring directly to files in the filesystem to # index.php. Clean URLs are handled in drupal_environment_initialize(). diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 7cb97d2..1d6f060 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -729,31 +729,19 @@ function drupal_get_filename($type, $name, $filename = NULL) { * file. */ function variable_initialize($conf = array()) { - // NOTE: caching the variables improves performance by 20% when serving - // cached pages. - if ($cached = cache_get('variables', 'cache_bootstrap')) { + $cache = &drupal_static('variable', array()); + if ($cached = cache_get('variable_cache', 'cache_bootstrap')) { $variables = $cached->data; } else { - // Cache miss. Avoid a stampede. - $name = 'variable_init'; - if (!lock_acquire($name, 1)) { - // Another request is building the variable cache. - // Wait, then re-run this function. - lock_wait($name); - return variable_initialize($conf); - } - else { - // Proceed with variable rebuild. - $variables = array_map('unserialize', db_query('SELECT name, value FROM {variable}')->fetchAllKeyed()); - cache_set('variables', $variables, 'cache_bootstrap'); - lock_release($name); - } + $variables = array(); } - foreach ($conf as $name => $value) { $variables[$name] = $value; } + // Set $cache['#initialized']. After variable_initialize(), variable_set() + // queries the database if variables aren't already in $conf. + $cache['#initialized'] = TRUE; return $variables; } @@ -778,6 +766,25 @@ function variable_initialize($conf = array()) { */ function variable_get($name, $default = NULL) { global $conf; + if (isset($conf[$name])) { + return $conf[$name]; + } + // Use the advanced drupal_static() pattern, since this is called very often. + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['cache'] = &drupal_static('variable', array()); + } + $cache = &$drupal_static_fast['cache']; + + // $cache is populated in variable_initialize() for each request. If it's not + // available here we are in a lower bootstrap phase where the database and + // cache system may not be available yet. + if (!empty($cache['#initialized']) && !array_key_exists($name, $conf)) { + // If the variable is not already cached, look it up from the database. + $result = db_query('SELECT value FROM {variable} WHERE name = :name', array(':name' => $name))->fetchField(); + $value = $result ? unserialize($result) : $default; + $conf[$name] = $cache[$name] = $value; + } return isset($conf[$name]) ? $conf[$name] : $default; } @@ -799,13 +806,10 @@ function variable_get($name, $default = NULL) { * @see variable_get() */ function variable_set($name, $value) { - global $conf; - db_merge('variable')->key(array('name' => $name))->fields(array('value' => serialize($value)))->execute(); - - cache_clear_all('variables', 'cache_bootstrap'); - - $conf[$name] = $value; + $GLOBALS['conf'][$name] = $value; + variable_write_cache('variable_cache', 'cache_bootstrap', 'set', $name, $value); + variable_reset_cache(FALSE); } /** @@ -822,14 +826,82 @@ function variable_set($name, $value) { * @see variable_set() */ function variable_del($name) { - global $conf; - db_delete('variable') ->condition('name', $name) ->execute(); - cache_clear_all('variables', 'cache_bootstrap'); + variable_write_cache('variable_cache', 'cache_bootstrap', 'del', $name); + unset($GLOBALS['conf'][$name]); + variable_reset_cache(FALSE); +} - unset($conf[$name]); +/** + * Reset the variable cache. + * + * @param $bootstrap + * Optional parameter to reset the variable cache for bootstrap. If a + * variable is not cached for bootstrap, there is no need to clear the cache. + * Defaults to TRUE. + */ +function variable_reset_cache($bootstrap = TRUE) { + if ($bootstrap) { + cache_clear_all('variable_cache', 'cache_bootstrap'); + } + cache_clear_all('variable_cache:', 'cache', TRUE); + $cache = &drupal_static('variable'); +} + +/** + * Update the variable cache after a cache miss, variable_set() or variable_del(). + * + * Since the bootstrap variables cache may have high contention, to avoid + * stampedes we updated the cached variables and refresh the entry after + * variable_set() and variable_del() instead of just clearing the cache. + * This means that subsequent requests will get a new copy of the cache instead + * of having to rebuild it from scratch. + * + * @param $cid + * The cache ID. + * @param $bin + * The cache bin. + * @param $op + * Optional, used only if called from variable_set() + * or variable_del(), can be 'set' or 'del'. + * @param $name + * Optional, the variable name. + * @param $value + * Optional, the value to set. + */ +function variable_write_cache($cid = 'variable_cache', $bin = 'cache_bootstrap', $op = NULL, $name = NULL, $value = NULL) { + $cache = &drupal_static('variable', array()); + $write = count($cache) > 1; + if ($write || $op) { + // Get a fresh copy of the variable cache from cache_get(), this will + // include any changes made by other requests between variable_initialize() + // and variable_write_cache(). + if ($cached = cache_get($cid, $bin)) { + $current_cache = $cached->data; + if ($op) { + $cache = $current_cache; + if ($op == 'del') { + if (array_key_exists($name, $cache)) { + $write = TRUE; + unset($cache[$name]); + } + } + elseif (array_key_exists($name, $cache)) { + $write = TRUE; + $cache[$name] = $value; + } + } + else { + $cache = array_merge($current_cache, $cache); + } + } + if ($write) { + cache_set($cid, $cache, $bin); + } + } + $cache = array('#initialized' => TRUE); } /** @@ -2057,6 +2129,8 @@ function _drupal_bootstrap_page_cache() { if (variable_get('page_cache_invoke_hooks', TRUE)) { bootstrap_invoke_all('exit'); } + // Allow the variables requested for cached pages to be cached. + variable_write_cache(); // We are done. exit; } @@ -2124,10 +2198,6 @@ function _drupal_bootstrap_database() { function _drupal_bootstrap_variables() { global $conf; - // Initialize the lock system. - require_once DRUPAL_ROOT . '/' . variable_get('lock_inc', 'includes/lock.inc'); - lock_initialize(); - // Load variables from the database, but do not overwrite variables set in settings.php. $conf = variable_initialize(isset($conf) ? $conf : array()); // Load bootstrap modules. @@ -2141,6 +2211,10 @@ function _drupal_bootstrap_variables() { function _drupal_bootstrap_page_header() { bootstrap_invoke_all('boot'); + // Prepare for non-cached page workflow. + require_once DRUPAL_ROOT . '/' . variable_get('lock_inc', 'includes/lock.inc'); + lock_initialize(); + if (!drupal_is_cli()) { ob_start(); drupal_page_header(); diff --git a/includes/common.inc b/includes/common.inc index b6ea297..ec791af 100644 --- a/includes/common.inc +++ b/includes/common.inc @@ -2567,6 +2567,7 @@ function drupal_page_footer() { drupal_cache_system_paths(); module_implements_write_cache(); system_run_automated_cron(); + variable_write_cache('variable_cache:' . arg(0), 'cache'); } /** @@ -2592,6 +2593,34 @@ function drupal_exit($destination = NULL) { } /** + * Initialize the variables cache after full bootstrap. + */ +function variable_initialize_full() { + // Allow variables fetched up to this point to be cached in the global + // variable_bootstrap cache. + // The cache system often relies on the variable system, so ensure that + // any variables needed for the cache_get() call are requested before we + // actually set the cache. + // After full bootstrap, we segment the variables cache by system path root. + // For example 'node' or 'admin'. + $cached = cache_get('variable_cache:' . arg(0), 'cache'); + variable_write_cache(); + $cache = &drupal_static('variable', array()); + if ($cached) { + $variables = $cached->data; + } + else { + $variables = array(); + } + + // Ensure existing variables in $conf override cached values. + foreach ($GLOBALS['conf'] as $name => $value) { + $variables[$name] = $value; + } + $GLOBALS['conf'] = $variables; +} + +/** * Form an associative array from a linear array. * * This function walks through the provided array and constructs an associative @@ -4931,6 +4960,7 @@ function _drupal_bootstrap_full() { menu_set_custom_theme(); drupal_theme_initialize(); module_invoke_all('init'); + variable_initialize_full(); } } diff --git a/modules/color/color.module b/modules/color/color.module index ff6c70e..aa13b45 100644 --- a/modules/color/color.module +++ b/modules/color/color.module @@ -159,7 +159,7 @@ function color_scheme_form($complete_form, &$form_state, $theme) { // See if we're using a predefined scheme. // Note: we use the original theme when the default scheme is chosen. - $current_scheme = variable_get('color_' . $theme . '_palette', array()); + $current_scheme = variable_get('color_' . $theme . '_palette'); foreach ($schemes as $key => $scheme) { if ($current_scheme == $scheme) { $scheme_name = $key; diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index b60c682..3e83528 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -1238,16 +1238,6 @@ class DrupalWebTestCase extends DrupalTestCase { ->condition('test_id', $this->testId) ->execute(); - // Clone the current connection and replace the current prefix. - $connection_info = Database::getConnectionInfo('default'); - Database::renameConnection('default', 'simpletest_original_default'); - foreach ($connection_info as $target => $value) { - $connection_info[$target]['prefix'] = array( - 'default' => $value['prefix']['default'] . $this->databasePrefix, - ); - } - Database::addConnectionInfo('default', 'default', $connection_info['default']); - // Store necessary current values before switching to prefixed database. $this->originalLanguage = $language; $this->originalLanguageDefault = variable_get('language_default'); @@ -1276,6 +1266,16 @@ class DrupalWebTestCase extends DrupalTestCase { file_prepare_directory($temp_files_directory, FILE_CREATE_DIRECTORY); $this->generatedTestFiles = FALSE; + // Clone the current connection and replace the current prefix. + $connection_info = Database::getConnectionInfo('default'); + Database::renameConnection('default', 'simpletest_original_default'); + foreach ($connection_info as $target => $value) { + $connection_info[$target]['prefix'] = array( + 'default' => $value['prefix']['default'] . $this->databasePrefix, + ); + } + Database::addConnectionInfo('default', 'default', $connection_info['default']); + // Log fatal errors. ini_set('log_errors', 1); ini_set('error_log', $public_files_directory . '/error.log'); @@ -1429,7 +1429,7 @@ class DrupalWebTestCase extends DrupalTestCase { */ protected function refreshVariables() { global $conf; - cache_clear_all('variables', 'cache_bootstrap'); + variable_reset_cache(); $conf = variable_initialize(); } diff --git a/modules/simpletest/tests/file.test b/modules/simpletest/tests/file.test index dc12b1b..0794722 100644 --- a/modules/simpletest/tests/file.test +++ b/modules/simpletest/tests/file.test @@ -2241,6 +2241,7 @@ class FileDownloadTest extends FileTestCase { // Deny access to all downloads via a -1 header. file_test_set_return('download', -1); + variable_reset_cache(); $this->drupalHead($url); $this->assertResponse(403, t('Correctly denied access to a file when file_test sets the header to -1.')); diff --git a/modules/simpletest/tests/upgrade/upgrade.test b/modules/simpletest/tests/upgrade/upgrade.test index 1ef8525..f118df9 100644 --- a/modules/simpletest/tests/upgrade/upgrade.test +++ b/modules/simpletest/tests/upgrade/upgrade.test @@ -33,6 +33,26 @@ abstract class UpgradePathTestCase extends DrupalWebTestCase { protected function setUp() { global $user, $language, $conf; + // Store necessary current values before switching to prefixed database. + $this->originalLanguage = $language; + $this->originalLanguageDefault = variable_get('language_default'); + $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); + $this->originalProfile = drupal_get_profile(); + $clean_url_original = variable_get('clean_url', 0); + + // Create test directories ahead of installation so fatal errors and debug + // information can be logged during installation process. + // Use mock files directories with the same prefix as the database. + $public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); + $private_files_directory = $public_files_directory . '/private'; + $temp_files_directory = $private_files_directory . '/temp'; + + // Create the directories. + file_prepare_directory($public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + file_prepare_directory($private_files_directory, FILE_CREATE_DIRECTORY); + file_prepare_directory($temp_files_directory, FILE_CREATE_DIRECTORY); + $this->generatedTestFiles = FALSE; + // Load the Update API. require_once DRUPAL_ROOT . '/includes/update.inc'; @@ -59,31 +79,11 @@ abstract class UpgradePathTestCase extends DrupalWebTestCase { } Database::addConnectionInfo('default', 'default', $connection_info['default']); - // Store necessary current values before switching to prefixed database. - $this->originalLanguage = $language; - $this->originalLanguageDefault = variable_get('language_default'); - $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files'); - $this->originalProfile = drupal_get_profile(); - $clean_url_original = variable_get('clean_url', 0); - // Unregister the registry. // This is required to make sure that the database layer works properly. spl_autoload_unregister('drupal_autoload_class'); spl_autoload_unregister('drupal_autoload_interface'); - // Create test directories ahead of installation so fatal errors and debug - // information can be logged during installation process. - // Use mock files directories with the same prefix as the database. - $public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10); - $private_files_directory = $public_files_directory . '/private'; - $temp_files_directory = $private_files_directory . '/temp'; - - // Create the directories. - file_prepare_directory($public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); - file_prepare_directory($private_files_directory, FILE_CREATE_DIRECTORY); - file_prepare_directory($temp_files_directory, FILE_CREATE_DIRECTORY); - $this->generatedTestFiles = FALSE; - // Log fatal errors. ini_set('log_errors', 1); ini_set('error_log', $public_files_directory . '/error.log'); diff --git a/modules/system/system.test b/modules/system/system.test index 583dd6d..28cfa66 100644 --- a/modules/system/system.test +++ b/modules/system/system.test @@ -616,6 +616,7 @@ class CronRunTestCase extends DrupalWebTestCase { // Test if cron runs when the cron threshold was passed. $cron_last = time() - 200; variable_set('cron_last', $cron_last); + variable_reset_cache(); $this->drupalGet(''); sleep(1); $this->assertTrue($cron_last < variable_get('cron_last', NULL), t('Cron runs when the cron threshold is passed.'));