diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc index 06f8c68..1f6db6a 100644 --- a/includes/bootstrap.inc +++ b/includes/bootstrap.inc @@ -226,6 +226,160 @@ 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; + + $cache_data = array(); + if ($cached = cache_get($this->cid, $this->bin)) { + foreach (cache_get_multiple($cached->data, $this->bin) as $cid => $item) { + $cache_data[$cid] = $item->data; + $this->keysToSave[$cid] = TRUE; + } + } + parent::__construct($cache_data); + } + + 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; + } + + public function exchangeArray($input) { + // Reset keysToSave to ensure there is no attempt to cache keys that are + // not part of the cache object. + $this->keysToSave = array(); + return parent::exchangeArray($input); + } + + /** + * 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. + */ + protected function set($cid, $data, $bin) { + cache_set($cid, $data, $bin); + } + + public function __destruct() { + $data = array(); + foreach ($this->keysToSave as $offset => $persist) { + if ($persist) { + $data[] = $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 +855,19 @@ function drupal_get_filename($type, $name, $filename = NULL) { } /** + * Extends CacheArrayObject to allow for cumulative caching of variables. + */ +class VariableCache extends CacheArrayObject { + protected function resolveCacheMiss($offset) { + $result = db_query('SELECT value FROM {variable} WHERE name = :name', array(':name' => $offset))->fetchField(); + $value = $result ? unserialize($result) : NULL; + cache_set($offset, $value, 'cache_bootstrap'); + 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,33 +875,14 @@ 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); - } - } + $cache = &drupal_static('variables'); + $cache = new VariableCache('variables', 'cache_bootstrap'); foreach ($conf as $name => $value) { - $variables[$name] = $value; + $cache[$name] = $value; } - return $variables; + return $cache; } /** @@ -757,8 +905,15 @@ function variable_initialize($conf = array()) { */ function variable_get($name, $default = NULL) { global $conf; - - return isset($conf[$name]) ? $conf[$name] : $default; + if (isset($conf[$name])) { + return $conf[$name]; + } + $cache = &drupal_static('variables'); + if (isset($cache) && isset($cache[$name])) { + $conf[$name] = $cache[$name]; + return $conf[$name]; + } + return $default; } /** @@ -781,10 +936,8 @@ 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; + cache_set($name, $value, 'cache_bootstrap'); } /** @@ -806,9 +959,8 @@ function variable_del($name) { db_delete('variable') ->condition('name', $name) ->execute(); - cache_clear_all('variables', 'cache_bootstrap'); - unset($conf[$name]); + cache_clear_all($name, 'cache_bootstrap'); } /** diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php index 40af458..ce3ff98 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;