summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorwebchick <webchick@24967.no-reply.drupal.org>2011-08-29 23:04:11 -0700
committerwebchick <webchick@24967.no-reply.drupal.org>2011-08-29 23:04:11 -0700
commitb9f87d85b5e9e8544b81d26ab17859a28ef64d17 (patch)
treecfb311d9748e17f31ad76cb9bbd5b25213e5c9b0
parent3a9a4ee0e68fdfa5da35a2f9e53e4f7d47dbf287 (diff)
downloadbrdo-b9f87d85b5e9e8544b81d26ab17859a28ef64d17.tar.gz
brdo-b9f87d85b5e9e8544b81d26ab17859a28ef64d17.tar.bz2
Issue #402896 by berdir, catch, andypost, dmitrig01, chx, pounard, sun, sdboyer: introduce DrupalCacheArray and use it for drupal_get_schema().
-rw-r--r--includes/bootstrap.inc251
1 files changed, 242 insertions, 9 deletions
diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc
index c7c484f83..43cbb1d85 100644
--- a/includes/bootstrap.inc
+++ b/includes/bootstrap.inc
@@ -226,6 +226,195 @@ define('REGISTRY_WRITE_LOOKUP_CACHE', 2);
define('DRUPAL_PHP_FUNCTION_PATTERN', '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*');
/**
+ * Provides a caching wrapper to be used in place of large array structures.
+ *
+ * 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 ArrayAccess 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 ArrayAccess. Systems using
+ * DrupalCacheArray should use this only internally. If providing API functions
+ * that return the full array, this can be cached separately or returned
+ * directly. However since DrupalCacheArray holds partial content by design, it
+ * should be a normal PHP array or otherwise contain the full structure.
+ *
+ * Note also that due to limitations in PHP prior to 5.3.4, it is impossible to
+ * write directly to the contents of nested arrays contained in this object.
+ * Only writes to the top-level array elements are possible. So if you
+ * previously had set $object['foo'] = array(1, 2, 'bar' => 'baz'), but later
+ * want to change the value of 'bar' from 'baz' to 'foobar', you cannot do so
+ * a targeted write like $object['foo']['bar'] = 'foobar'. Instead, you must
+ * overwrite the entire top-level 'foo' array with the entire set of new
+ * values: $object['foo'] = array(1, 2, 'bar' => 'foobar'). Due to this same
+ * limitation, attempts to create references to any contained data, nested or
+ * otherwise, will fail silently. So $var = &$object['foo'] will not throw an
+ * error, and $var will be populated with the contents of $object['foo'], but
+ * that data will be passed by value, not reference. For more information on
+ * the PHP limitation, see the note in the official PHP documentation at·
+ * http://php.net/manual/en/arrayaccess.offsetget.php on
+ * ArrayAccess::offsetGet().
+ *
+ * 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 DrupalCacheArray implements ArrayAccess {
+
+ /**
+ * 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 $keysToPersist = array();
+
+ /**
+ * Storage for the data itself.
+ */
+ protected $storage = 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)) {
+ $this->storage = $cached->data;
+ }
+ }
+
+ public function offsetExists($offset) {
+ return $this->offsetGet($offset) !== NULL;
+ }
+
+ public function offsetGet($offset) {
+ if (isset($this->storage[$offset]) || array_key_exists($offset, $this->storage)) {
+ return $this->storage[$offset];
+ }
+ else {
+ return $this->resolveCacheMiss($offset);
+ }
+ }
+
+ public function offsetSet($offset, $value) {
+ $this->storage[$offset] = $value;
+ }
+
+ public function offsetUnset($offset) {
+ unset($this->storage[$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->keysToPersist[$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.
+ *
+ * @return
+ * The value of the offset, or NULL if no value was found.
+ */
+ 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 cache writes to help avoid stampedes.
+ // To implement locking for cache misses, override __construct().
+ $lock_name = $cid . ':' . $bin;
+ 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->keysToPersist as $offset => $persist) {
+ if ($persist) {
+ $data[$offset] = $this->storage[$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.
*
@@ -2532,6 +2721,55 @@ function ip_address() {
* If true, the schema will be rebuilt instead of retrieved from the cache.
*/
function drupal_get_schema($table = NULL, $rebuild = FALSE) {
+ static $schema;
+
+ if ($rebuild || !isset($table)) {
+ $schema = drupal_get_complete_schema($rebuild);
+ }
+ elseif (!isset($schema)) {
+ $schema = new SchemaCache();
+ }
+
+ if (!isset($table)) {
+ return $schema;
+ }
+ if (isset($schema[$table])) {
+ return $schema[$table];
+ }
+ else {
+ return FALSE;
+ }
+}
+
+/**
+ * Extends DrupalCacheArray to allow for dynamic building of the schema cache.
+ */
+class SchemaCache extends DrupalCacheArray {
+
+ public function __construct() {
+ // Cache by request method.
+ parent::__construct('schema:runtime:' . $_SERVER['REQUEST_METHOD'] == 'GET', 'cache');
+ }
+
+ protected function resolveCacheMiss($offset) {
+ $complete_schema = drupal_get_complete_schema();
+ $value = isset($complete_schema[$offset]) ? $complete_schema[$offset] : NULL;
+ $this->storage[$offset] = $value;
+ $this->persist($offset);
+ return $value;
+ }
+}
+
+/**
+ * Get the whole database schema.
+ *
+ * The returned schema will include any modifications made by any
+ * module that implements hook_schema_alter().
+ *
+ * @param $rebuild
+ * If true, the schema will be rebuilt instead of retrieved from the cache.
+ */
+function drupal_get_complete_schema($rebuild = FALSE) {
static $schema = array();
if (empty($schema) || $rebuild) {
@@ -2573,18 +2811,13 @@ function drupal_get_schema($table = NULL, $rebuild = FALSE) {
if (!empty($schema) && (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL)) {
cache_set('schema', $schema);
}
+ if ($rebuild) {
+ cache_clear_all('schema:', 'cache', TRUE);
+ }
}
}
- if (!isset($table)) {
- return $schema;
- }
- elseif (isset($schema[$table])) {
- return $schema[$table];
- }
- else {
- return FALSE;
- }
+ return $schema;
}
/**