From c8ee2170fdc39e8f6a02f5418adc5055508b9a05 Mon Sep 17 00:00:00 2001 From: mixologic Date: Wed, 30 Apr 2014 17:55:24 -0700 Subject: [PATCH] Reroll Merge with HEAD for 1807662 --- core/core.services.yml | 2 + core/lib/Drupal/Core/Cache/ApcuBackend.php | 380 +++++++++++++++++++++ core/lib/Drupal/Core/Cache/ApcuBackendFactory.php | 39 +++ .../system/Tests/Cache/ApcuBackendUnitTest.php | 56 +++ 4 files changed, 477 insertions(+) create mode 100644 core/lib/Drupal/Core/Cache/ApcuBackend.php create mode 100644 core/lib/Drupal/Core/Cache/ApcuBackendFactory.php create mode 100644 core/modules/system/lib/Drupal/system/Tests/Cache/ApcuBackendUnitTest.php diff --git a/core/core.services.yml b/core/core.services.yml index 19b0b72..703d255 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -25,6 +25,8 @@ services: cache.backend.database: class: Drupal\Core\Cache\DatabaseBackendFactory arguments: ['@database'] + cache.backend.apcu: + class: Drupal\Core\Cache\ApcuBackendFactory cache.backend.php: class: Drupal\Core\Cache\PhpBackendFactory cache.bootstrap: diff --git a/core/lib/Drupal/Core/Cache/ApcuBackend.php b/core/lib/Drupal/Core/Cache/ApcuBackend.php new file mode 100644 index 0000000..f6de468 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/ApcuBackend.php @@ -0,0 +1,380 @@ +bin = $bin; + $this->sitePrefix = $site_prefix; + $this->binPrefix = $this->sitePrefix . '::' . $this->bin . '::'; + $this->tagsPrefix = $this->sitePrefix . '::tags::'; + } + + /** + * Prepends the APC user variable prefix for this bin to a cache item ID. + * + * @param string $cid + * The cache item ID to prefix. + * + * @return string + * The APCu key for the cache item ID. + */ + protected function getApcuKey($cid) { + return $this->binPrefix . $cid; + } + + /** + * {@inheritdoc} + */ + public function get($cid, $allow_invalid = FALSE) { + $cache = apc_fetch($this->getApcuKey($cid)); + return $this->prepareItem($cache, $allow_invalid); + } + + /** + * {@inheritdoc} + */ + public function getMultiple(&$cids, $allow_invalid = FALSE) { + // Translate the requested cache item IDs to APCu keys. + $map = array(); + foreach ($cids as $cid) { + $map[$this->getApcuKey($cid)] = $cid; + } + + $result = apc_fetch(array_keys($map)); + $cache = array(); + foreach ($result as $key => $item) { + $item = $this->prepareItem($item, $allow_invalid); + if ($item) { + $cache[$map[$key]] = $item; + } + } + unset($result); + + $cids = array_diff($cids, array_keys($cache)); + return $cache; + } + + /** + * Returns all cached items, optionally limited by a cache ID prefix. + * + * APCu is a memory cache, shared across all server processes. To prevent + * cache item clashes with other applications/installations, every cache item + * is prefixed with a unique string for this site. Therefore, functions like + * apc_clear_cache() cannot be used, and instead, a list of all cache items + * belonging to this application need to be retrieved through this method + * instead. + * + * @param string $prefix + * (optional) A cache ID prefix to limit the result to. + * + * @return \APCIterator + * An APCIterator containing matched items. + */ + public function getAll($prefix = '') { + return new \APCIterator('user', '/^' . preg_quote($this->getApcuKey($prefix), '/') . '/'); + } + + /** + * Prepares a cached item. + * + * Checks that the item is either permanent or did not expire. + * + * @param \stdClass $cache + * An item loaded from cache_get() or cache_get_multiple(). + * @param bool $allow_invalid + * If TRUE, a cache item may be returned even if it is expired or has been + * invalidated. See ::get(). + * + * @return mixed + * The cache item or FALSE if the item expired. + */ + protected function prepareItem($cache, $allow_invalid) { + if (!isset($cache->data)) { + return FALSE; + } + + $cache->tags = $cache->tags ? explode(' ', $cache->tags) : array(); + $checksum = $this->checksumTags($cache->tags); + + // The cache data is invalid if any of its tags have been cleared since. + if (!$this->validTags($checksum, $cache->tags)) { + return FALSE; + } + + // Check expire time. + $cache->valid = ($cache->expire == CacheBackendInterface::CACHE_PERMANENT) || ($cache->expire >= REQUEST_TIME); + + // Check if invalidateTags() has been called with any of the entry's tags. + if ($cache->checksum != $checksum) { + $cache->valid = FALSE; + } + + if (!$allow_invalid && !$cache->valid) { + return FALSE; + } + + return $cache; + } + + /** + * {@inheritdoc} + */ + public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = array()) { + $cache = new \stdClass(); + $cache->cid = $cid; + $cache->created = REQUEST_TIME; + $cache->expire = $expire; + $cache->tags = implode(' ', $this->flattenTags($tags)); + $cache->checksum = $this->checksumTags($tags); + + // APC serializes/unserializes any structure itself. + $cache->serialized = 0; + $cache->data = $data; + + // apc_store()'s $ttl argument can be omitted but also set to 0 (zero), + // in which case the value will persist until it's removed from the cache or + // until the next cache clear, restart, etc. This is what we want to do + // when $expire equals CacheBackendInterface::CACHE_PERMANENT. + if ($expire === CacheBackendInterface::CACHE_PERMANENT) { + $expire = 0; + } + apc_store($this->getApcuKey($cid), $cache, $expire); + } + + /** + * {@inheritdoc + */ + public function setMultiple(array $items = array()) { + foreach ($items as $cid => $item) { + $this->set($cid, $item['data'], isset($item['expire']) ? $item['expire'] : CacheBackendInterface::CACHE_PERMANENT, isset($item['tags']) ? $item['tags'] : array()); + } + } + + /** + * {@inheritdoc} + */ + public function delete($cid) { + apc_delete($this->getApcuKey($cid)); + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple(array $cids) { + apc_delete(array_map(array($this, 'getApcuKey'), $cids)); + } + + /** + * {@inheritdoc} + */ + public function deleteAll() { + apc_delete(new \APCIterator('user', '/^' . preg_quote($this->binPrefix, '/') . '/')); + } + + /** + * {@inheritdoc} + */ + public function garbageCollection() { + // Any call to apc_fetch() causes APC to expunge expired items. + apc_fetch(''); + } + + /** + * {@inheritdoc} + */ + public function removeBin() { + $this->deleteAll(); + apc_delete(new \APCIterator('user', '/^' . preg_quote($this->tagsPrefix, '/') . '/')); + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + return $this->getAll()->getTotalCount() === 0; + } + + /** + * {@inheritdoc} + */ + public function invalidate($cid) { + $this->invalidateMultiple(array($cid)); + } + + /** + * {@inheritdoc} + */ + public function invalidateMultiple(array $cids) { + foreach ($this->getMultiple($cids) as $cache) { + $cache->expire = REQUEST_TIME - 1; + $this->set($cache->cid, $cache); + } + } + + /** + * {@inheritdoc} + */ + public function invalidateAll() { + foreach ($this->getAll() as $data) { + $data['value']->expire = REQUEST_TIME - 1; + $this->set($data['key'], $data['value']); + } + } + + /** + * {@inheritdoc} + */ + public function deleteTags(array $tags) { + foreach ($this->flattenTags($tags) as $tag) { + apc_delete($this->tagsPrefix . $tag); + unset(static::$tagCache[$tag]); + } + } + + /** + * {@inheritdoc} + */ + public function invalidateTags(array $tags) { + foreach ($this->flattenTags($tags) as $tag) { + unset(static::$tagCache[$tag]); + apc_inc($this->tagsPrefix . $tag, 1, $success); + if (!$success) { + apc_store($this->tagsPrefix . $tag, 1); + } + } + } + + /** + * Compares two checksums of tags. + * + * Used to determine whether to serve a cached item or treat it as + * invalidated. + * + * @param int $checksum + * The initial checksum to compare against. + * @param array $tags + * An array of tags to calculate a checksum for. + * + * @return bool + * TRUE if the checksums match, FALSE otherwise. + */ + protected function validTags($checksum, array $tags) { + return $checksum == $this->checksumTags($tags); + } + + /** + * Flattens a tags array into a numeric array suitable for string storage. + * + * @param array $tags + * Associative array of tags to flatten. + * + * @return array + * Indexed array of flattened tag identifiers. + */ + protected function flattenTags(array $tags) { + if (isset($tags[0])) { + return $tags; + } + + $flat_tags = array(); + foreach ($tags as $namespace => $values) { + if (is_array($values)) { + foreach ($values as $value) { + $flat_tags[] = "$namespace:$value"; + } + } + else { + $flat_tags[] = "$namespace:$values"; + } + } + return $flat_tags; + } + + /** + * Returns the sum total of validations for a given set of tags. + * + * @param array $tags + * Associative array of tags. + * + * @return int + * Sum of all invalidations. + */ + protected function checksumTags(array $tags) { + $checksum = 0; + $query_tags = array(); + + foreach ($this->flattenTags($tags) as $tag) { + if (isset(static::$tagCache[$tag])) { + $checksum += static::$tagCache[$tag]; + } + else { + $query_tags[] = $this->tagsPrefix . $tag; + } + } + if ($query_tags) { + $result = apc_fetch($query_tags); + static::$tagCache = array_merge(static::$tagCache, $result); + $checksum += array_sum($result); + } + + return $checksum; + } + +} diff --git a/core/lib/Drupal/Core/Cache/ApcuBackendFactory.php b/core/lib/Drupal/Core/Cache/ApcuBackendFactory.php new file mode 100644 index 0000000..6c6d4ba --- /dev/null +++ b/core/lib/Drupal/Core/Cache/ApcuBackendFactory.php @@ -0,0 +1,39 @@ +sitePrefix = md5(DRUPAL_ROOT . '/' . conf_path()); + } + + /** + * Gets ApcuBackend for the specified cache bin. + * + * @param $bin + * The cache bin for which the object is created. + * + * @return \Drupal\Core\Cache\ApcuBackend + * The cache backend object for the specified cache bin. + */ + public function get($bin) { + return new ApcuBackend($bin, $this->sitePrefix); + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Cache/ApcuBackendUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Cache/ApcuBackendUnitTest.php new file mode 100644 index 0000000..8da6243 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Cache/ApcuBackendUnitTest.php @@ -0,0 +1,56 @@ + 'APCu cache backend', + 'description' => 'Tests the APCu cache backend.', + 'group' => 'Cache', + ); + } + + protected function checkRequirements() { + $requirements = parent::checkRequirements(); + if (!extension_loaded('apc')) { + $requirements[] = 'APC extension not found.'; + } + else { + if (version_compare(phpversion('apc'), '3.1.1', '<')) { + $requirements[] = 'APC extension must be newer than 3.1.1 for APCIterator support.'; + } + if (PHP_SAPI === 'cli' && !ini_get('apc.enable_cli')) { + $requirements[] = 'apc.enable_cli must be enabled to run this test.'; + } + } + return $requirements; + } + + protected function createCacheBackend($bin) { + return new ApcuBackend($bin, 'test'); + } + +} -- 1.8.5.3