diff --git a/core/lib/Drupal/Core/Cache/ApcBackend.php b/core/lib/Drupal/Core/Cache/ApcBackend.php new file mode 100644 index 0000000..d257167 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/ApcBackend.php @@ -0,0 +1,384 @@ +bin = $bin; + + // Determine the global APC user variable prefix. + $prefix = self::getPrefix($this->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::'; + } + + /** + * Determines the APC user variable prefix to use. + * + * @param string $bin + * The name of the cache bin to return a prefix for. + * + * @return string + * The APC user variable prefix to use; may be enforced to an empty string. + * This prefix is not specific to a cache bin; the cache bin name is + * appended later on. + * + * This utility method is static to allow to determine the prefix elsewhere. + */ + public static function getPrefix($bin) { + $prefixes = variable_get('cache_apc_prefix'); + + if (isset($prefixes)) { + // If it is a string, the same prefix is used for all bins. + if (is_string($prefixes)) { + $prefix = $prefixes; + } + elseif (isset($prefixes[$bin])) { + // An explicit FALSE means no prefix. + $prefix = ($prefixes[$bin] === FALSE ? '' : $prefixes[$bin]); + } + elseif (isset($prefixes['default'])) { + // An explicit FALSE means no prefix. + $prefix = ($prefixes['default'] === FALSE ? '' : $prefixes['default']); + } + } + // If no prefix has been configured, default to HTTP_HOST. + if (!isset($prefix) && isset($_SERVER['HTTP_HOST'])) { + $prefix = $_SERVER['HTTP_HOST']; + } + // If there is not even a HTTP_HOST fallback, there is no safe default. + else { + throw new \RuntimeException(format_string('Unable to determine APC user variable prefix for %bin cache bin.', array('%bin' => $bin))); + } + $prefix .= '::'; + + // Prefix the prefix with the database prefix during testing. + // @todo drupal_valid_test_ua() does not always return the correct prefix, + // so the fallback to the global variable is required. + // @see http://drupal.org/node/1436684 + if ($test_prefix = drupal_valid_test_ua()) { + $prefix = $test_prefix . '::' . $prefix; + } + elseif (isset($GLOBALS['drupal_test_info'])) { + $prefix = $GLOBALS['drupal_test_info']['test_run_id'] . '::' . $prefix; + } + + return $prefix; + } + + /** + * 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; + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::get(). + */ + public function get($cid) { + $cache = apc_fetch($this->getApcKey($cid)); + return $this->prepareItem($cache); + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::getMultiple(). + */ + public function getMultiple(&$cids) { + // 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) { + 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; + } + } + + return $cache; + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::set(). + */ + 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); + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::delete(). + */ + public function delete($cid) { + apc_delete($this->getApcKey($cid)); + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::deleteMultiple(). + */ + public function deleteMultiple(array $cids) { + apc_delete(array_map(array($this, 'getApcKey'), $cids)); + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::flush(). + */ + public function flush() { + $iterator = $this->getAll(); + foreach ($iterator as $key => $data) { + apc_delete($key); + } + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::expire(). + */ + public function expire() { + // Any call to apc_fetch() causes APC to expunge expired items. + apc_fetch(''); + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::garbageCollection(). + */ + public function garbageCollection() { + $this->expire(); + } + + /** + * 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; + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::invalidateTags(). + */ + 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); + } + } + } + + /** + * 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; + } + + /** + * Implements Drupal\Core\Cache\CacheBackendInterface::isEmpty(). + */ + public function isEmpty() { + return $this->getAll()->getTotalCount() === 0; + } +} + +// PHP CLI uses a different php.ini in which APC may not be enabled. +// Create stub functions to prevent fatal errors. +if (drupal_is_cli() && (!extension_loaded('apc') || !ini_get('apc.enable_cli'))) { + function apc_fetch($key, &$success) { + $success = TRUE; + return FALSE; + } + function apc_store($key, $var) { + return FALSE; + } + function apc_delete($key) { + return FALSE; + } + function apc_inc($key, $step, &$success) { + $success = TRUE; + return FALSE; + } +} 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..db50165 --- /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 2395a84..a304e52 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. @@ -242,7 +241,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'; @@ -282,7 +281,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'; @@ -447,9 +446,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; }