diff --git a/core/lib/Drupal/Core/Cache/APCBackend.php b/core/lib/Drupal/Core/Cache/APCBackend.php new file mode 100644 index 0000000..baf8505 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/APCBackend.php @@ -0,0 +1,340 @@ +bin = $bin; + $this->prefix = $prefix; + + // Set the bin-specific prefix. + $this->binPrefix = $this->prefix . $this->bin . '::'; + // Set the cache tags bin prefix. + $this->tagsPrefix = $this->prefix . '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 APC key for the cache item ID. + */ + protected function getAPCKey($cid) { + return $this->binPrefix . $cid; + } + + /** + * {@inheritdoc} + */ + public function get($cid, $allow_invalid = FALSE) { + $cache = apc_fetch($this->getAPCKey($cid)); + return $this->prepareItem($cache, $allow_invalid); + } + + /** + * {@inheritdoc} + */ + public function getMultiple(&$cids, $allow_invalid = FALSE) { + // Translate the requested cache item IDs to APC keys. + $map = array(); + foreach ($cids as $cid) { + $map[$this->getAPCKey($cid)] = $cid; + } + + $result = apc_fetch(array_keys($map)); + $cache = array(); + foreach ($result as $key => $item) { + $item = $this->prepareItem($item); + 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. + * + * APC 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 application. 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->binPrefix . $prefix, '/') . '/', APC_ITER_KEY); + } + + /** + * Prepares a cached item. + * + * Checks that items are either permanent or did not expire. + * + * @param stdClass $cache + * An item loaded from cache_get() or cache_get_multiple(). + * + * @return mixed + * The item with data unserialized as appropriate or FALSE if there is no + * valid item to load. + */ + protected function prepareItem($cache, $allow_invalid) { + if (!isset($cache->data)) { + return FALSE; + } + + // The cache data is invalid if any of its tags have been cleared since. + if ($cache->tags) { + $cache->tags = explode(' ', $cache->tags); + if (!$this->validTags($cache->checksum, $cache->tags)) { + return 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), + // which happens to be identical to CACHE_PERMANENT. + apc_store($this->getAPCKey($cid), $cache, $expire); + } + + /** + * {@inheritdoc} + */ + public function delete($cid) { + apc_delete($this->getAPCKey($cid)); + } + + /** + * {@inheritdoc} + */ + public function deleteMultiple(array $cids) { + apc_delete(array_map(array($this, 'getAPCKey'), $cids)); + } + + /** + * {@inheritdoc} + */ + public function deleteAll() { + foreach ($this->getAll() as $key => $data) { + apc_delete($key); + } + } + + /** + * {@inheritdoc} + */ + public function garbageCollection() { + // Any call to apc_fetch() causes APC to expunge expired items. + apc_fetch(''); + } + + /** + * {@inheritdoc} + */ + public function removeBin() { + // @todo Just flush here instead? or do nothing? + } + + /** + * {@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 $cache) { + $cache->expire = REQUEST_TIME - 1; + $this->set($cache->cid, $cache); + } + } + + /** + * {@inheritdoc} + */ + public function invalidateTags(array $tags) { + foreach ($this->flattenTags($tags) as $tag) { + unset(self::$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 integer $checksum + * The initial checksum to compare against. + * @param array $tags + * An array of tags to calculate a checksum for. + * + * @return boolean + * 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 + * Numeric 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 integer + * Sum of all invalidations. + */ + protected function checksumTags($tags) { + $checksum = 0; + $query_tags = array(); + + foreach ($this->flattenTags($tags) as $tag) { + if (isset(self::$tagCache[$tag])) { + $checksum += self::$tagCache[$tag]; + } + else { + $query_tags[] = $this->tagsPrefix . $tag; + } + } + if ($query_tags) { + $result = apc_fetch($query_tags); + self::$tagCache = array_merge(self::$tagCache, $result); + $checksum += array_sum($result); + } + return $checksum; + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Cache/APCBackendUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Cache/APCBackendUnitTest.php new file mode 100644 index 0000000..0c2b085 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Cache/APCBackendUnitTest.php @@ -0,0 +1,55 @@ + 'APC cache backend', + 'description' => 'Tests the APC 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 (drupal_is_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 APCBackend($bin); + } +} diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index 49b1a44..fda80fb 100755 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -17,14 +17,13 @@ } if ($args['execute-test']) { - // Masquerade as Apache for running tests. - simpletest_script_init("Apache"); + simpletest_script_init(); simpletest_script_run_one_test($args['test-id'], $args['execute-test']); // Sub-process script execution ends here. } else { // Run administrative functions as CLI. - simpletest_script_init(NULL); + simpletest_script_init(); } // Bootstrap to perform initial validation or other operations. @@ -257,7 +256,7 @@ function simpletest_script_parse_args() { /** * Initialize script variables and perform general setup requirements. */ -function simpletest_script_init($server_software) { +function simpletest_script_init() { global $args, $php; $host = 'localhost'; @@ -299,7 +298,7 @@ function simpletest_script_init($server_software) { $_SERVER['HTTP_HOST'] = $host; $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $_SERVER['SERVER_ADDR'] = '127.0.0.1'; - $_SERVER['SERVER_SOFTWARE'] = $server_software; + $_SERVER['SERVER_SOFTWARE'] = NULL; $_SERVER['SERVER_NAME'] = 'localhost'; $_SERVER['REQUEST_URI'] = $path .'/'; $_SERVER['REQUEST_METHOD'] = 'GET'; @@ -550,9 +549,13 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) { // Retrieve the last database prefix used for testing. list($db_prefix, ) = simpletest_last_test_get($test_id); - // If no database prefix was found, then the test was not set up correctly. + // If no database prefix was found, then the test was not set up correctly, + // unless the test process exited successfully, in which case the test only + // failed a requirements check. if (empty($db_prefix)) { - echo "\nFATAL $test_class: Found no database prefix for test ID $test_id. (Check whether setUp() is invoked correctly.)"; + if ($exitcode) { + echo "\nFATAL $test_class: Found no database prefix for test ID $test_id. (Check whether setUp() is invoked correctly.)"; + } return; }