diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 06f8c68..4e7c898 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -226,6 +226,169 @@ define('REGISTRY_WRITE_LOOKUP_CACHE', 2); define('DRUPAL_PHP_FUNCTION_PATTERN', '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'); /** + * Extends ArrayObject to enable it to be used as a caching wrapper. + * + * This class should be extended by systems that need to cache large amounts + * of data and have it represented as an array to calling functions. These + * arrays can become very large, so ArrayObject is used to allow different + * strategies to be used for caching internally (lazy loading, building caches + * over time etc.). This can dramatically reduce the amount of data that needs + * to be loaded from cache backends on each request, and memory usage from + * static caches of that same data. + * + * Note that array_* functions do not work with ArrayObject. Systems using + * CacheArrayObject should use this only internally. If providing API functions + * that return the full array, this can be cached separately or returned + * directly. However since CacheArrayObject holds partial content by design, it + * should be a normal PHP array or otherwise contain the full structure. + * + * By default, the class accounts for caches where calling functions might + * request keys in the array that won't exist even after a cache rebuild. This + * prevents situations where a cache rebuild would be triggered over and over + * due to a 'missing' item. These cases are stored internally as a value of + * NULL. This means that the offsetGet() and offsetExists() methods + * must be overridden if caching an array where the top level values can + * legitimately be NULL, and where $object->offsetExists() needs to correctly + * return (equivalent to array_key_exists() vs. isset()). This should not + * be necessary in the majority of cases. + * + * Classes extending this class must override at least the + * resolveCacheMiss() method to have a working implementation. + * + * offsetSet() is not overridden by this class by default. In practice this + * means that assigning an offset via arrayAccess will only apply while the + * object is in scope and will not be written back to the persistent cache. + * This follows a similar pattern to static vs. persistent caching in + * procedural code. Extending classes may wish to alter this behaviour, for + * example by overriding offsetSet() and adding an automatic call to persist(). + * + * @see SchemaCache + */ +abstract class CacheArrayObject extends ArrayObject { + + /** + * A cid to pass to cache_set() and cache_get(). + */ + private $cid; + + /** + * A bin to pass to cache_set() and cache_get(). + */ + private $bin; + + /** + * An array of keys to add to the cache at the end of the request. + */ + protected $keysToSave = array(); + + /** + * Constructor. + * + * @param $cid + * The cid for the array being cached. + * @param $bin + * The bin to cache the array. + */ + public function __construct($cid, $bin) { + $this->cid = $cid; + $this->bin = $bin; + + if ($cached = cache_get($this->cid, $this->bin)) { + parent::__construct($cached->data); + } + else { + parent::__construct(array()); + } + } + + public function offsetExists($offset) { + return $this->offsetGet($offset) !== NULL; + } + + public function offsetGet($offset) { + if (!parent::offsetExists($offset)) { + $this->resolveCacheMiss($offset); + } + return parent::offsetGet($offset); + } + + /** + * Flags an offset value to be written to the persistent cache. + * + * If a value is assigned to a cache object with offsetSet(), by default it + * will not be written to the persistent cache unless it is flagged with this + * method. This allows items to be cached for the duration of a request, + * without necessarily writing back to the persistent cache at the end. + * + * @param $offset + * The array offset that was request. + * @param $persist + * Optional boolean to specify whether the offset should be persisted or + * not, defaults to TRUE. When called with $persist = FALSE the offset will + * be unflagged so that it will not written at the end of the request. + */ + protected function persist($offset, $persist = TRUE) { + $this->keysToSave[$offset] = $persist; + } + + /** + * Resolves a cache miss. + * + * When an offset is not found in the object, this is treated as a cache + * miss. This method allows classes implementing the interface to look up + * the actual value and allow it to be cached. + * + * @param $offset + * The offset that was requested. + */ + abstract protected function resolveCacheMiss($offset); + + /** + * Immediately write a value to the persistent cache. + * + * @param $cid + * The cache ID. + * @param $bin + * The cache bin. + * @param $data + * The data to write to the persistent cache. + * @param $lock + * Whether to acquire a lock before writing to cache. + */ + protected function set($cid, $data, $bin, $lock = TRUE) { + $lock_name = $cid . ':' . $bin; + // Since this method merges with the existing cache entry if it exists, + // ensure that only one process can update the cache item at any one time. + // This ensures that different requests can't overwrite each others' + // partial version of the cache and should help to avoid stampedes. + // When a lock cannot be acquired, the cache will not be written by + // that request. To implement locking for cache misses, override + // __construct(). + if (!$lock || lock_acquire($lock_name)) { + if ($cached = cache_get($cid, $bin)) { + $data = $cached->data + $data; + } + cache_set($cid, $data, $bin); + if ($lock) { + lock_release($lock_name); + } + } + } + + public function __destruct() { + $data = array(); + foreach ($this->keysToSave as $offset => $persist) { + if ($persist) { + $data[$offset] = parent::offsetGet($offset); + } + } + if (!empty($data)) { + $this->set($this->cid, $data, $this->bin); + } + } +} + +/** * Start the timer with the specified name. If you start and stop the same * timer multiple times, the measured intervals will be accumulated. * @@ -701,6 +864,27 @@ function drupal_get_filename($type, $name, $filename = NULL) { } /** + * Extends CacheArrayObject to allow for cumulative caching of variables. + */ +class VariableCache extends CacheArrayObject { + /** + * Redefine persist() as public. + * + * Allows variable_set() to directly manipulate cache. + */ + public function persist($offset, $persist = TRUE) { + parent::persist($offset, $persist); + } + + function resolveCacheMiss($offset) { + $result = db_query('SELECT value FROM {variable} WHERE name = :name', array(':name' => $offset))->fetchField(); + $value = $result ? unserialize($result) : NULL; + parent::offsetSet($offset, $value); + $this->persist($offset); + } +} + +/** * Load the persistent variable table. * * The variable table is composed of values that have been saved in the table @@ -708,28 +892,8 @@ 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')) { - $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 = new VariableCache('variables', 'cache_bootstrap'); foreach ($conf as $name => $value) { $variables[$name] = $value; } @@ -782,9 +946,16 @@ function variable_set($name, $value) { db_merge('variable')->key(array('name' => $name))->fields(array('value' => serialize($value)))->execute(); - cache_clear_all('variables', 'cache_bootstrap'); - $conf[$name] = $value; + + // Under some circumstances, such as the installer, variable_set() is called + // before variables have been initialized. + if (is_object($conf)) { + $conf->persist($name); + } + else { + cache_clear_all('variables', 'cache_bootstrap'); + } } /** @@ -806,9 +977,8 @@ function variable_del($name) { db_delete('variable') ->condition('name', $name) ->execute(); - cache_clear_all('variables', 'cache_bootstrap'); - unset($conf[$name]); + cache_clear_all('variables', 'cache_bootstrap'); } /** diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index 40af458..81678a9 100644 --- a/modules/simpletest/drupal_web_test_case.php +++ b/modules/simpletest/drupal_web_test_case.php @@ -1238,6 +1238,10 @@ class DrupalWebTestCase extends DrupalTestCase { ->condition('test_id', $this->testId) ->execute(); + // Reset all statics and variables to perform tests in a clean environment. + $conf = array(); + drupal_static_reset(); + // Clone the current connection and replace the current prefix. $connection_info = Database::getConnectionInfo('default'); Database::renameConnection('default', 'simpletest_original_default'); @@ -1280,10 +1284,6 @@ class DrupalWebTestCase extends DrupalTestCase { ini_set('log_errors', 1); ini_set('error_log', $public_files_directory . '/error.log'); - // Reset all statics and variables to perform tests in a clean environment. - $conf = array(); - drupal_static_reset(); - // Set the test information for use in other parts of Drupal. $test_info = &$GLOBALS['drupal_test_info']; $test_info['test_run_id'] = $this->databasePrefix; @@ -1345,7 +1345,7 @@ class DrupalWebTestCase extends DrupalTestCase { variable_set('site_mail', 'simpletest@example.com'); variable_set('date_default_timezone', date_default_timezone_get()); // Set up English language. - unset($GLOBALS['conf']['language_default']); + $GLOBALS['conf']['language_default'] = NULL; $language = language_default(); // Use the test mail class instead of the default mail handler class.