From fa379abcba03b0ccd75af8cc68eff958555d4596 Mon Sep 17 00:00:00 2001 From: Paulo Magalhaes Date: Sun, 23 Jan 2022 22:29:32 +0000 Subject: [PATCH 1/4] Fix APC cache tests - Using negative TTLs to force the immediate expiration of keys, while convenient in tests, doesn't work consistently with APC and is an undocumented feature. Using a low TTL and sleep() is what guarantees that it works for APC. See https://github.com/krakjoe/apcu/issues/184 - The setting apc.use_request_time interferes with key expiration when running on the CLI. Making sure it always has a sensible value for running the tests. See https://github.com/krakjoe/apcu/pull/392 --- .env.dist | 6 ------ docker-compose.yml | 4 ++-- test/unit/cache/sfAPCCacheTest.php | 4 ++++ test/unit/cache/sfCacheDriverTests.class.php | 18 ++++++++++++------ 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.env.dist b/.env.dist index 14b756ef5..c4d23d59a 100644 --- a/.env.dist +++ b/.env.dist @@ -3,9 +3,3 @@ # # Copy to `.env` in order to use it. # - -# APC test are disabled. -# -# To enable them in order to provide a fix, set to "on". -# -APC_ENABLE_CLI=off diff --git a/docker-compose.yml b/docker-compose.yml index 1814cef2a..ce0823a88 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: echo 'short_open_tag = off' echo 'magic_quotes_gpc = off' echo 'date.timezone = "UTC"' - echo 'apc.enable_cli = ${APC_ENABLE_CLI-off}' + echo 'apc.enable_cli = on' echo 'apc.use_request_time = 0' } | tee -a /usr/local/lib/php.ini @@ -60,7 +60,7 @@ services: echo 'short_open_tag = off' echo 'magic_quotes_gpc = off' echo 'date.timezone = "UTC"' - echo 'apc.enable_cli = ${APC_ENABLE_CLI-off}' + echo 'apc.enable_cli = on' echo 'apc.use_request_time = 0' } | tee -a /usr/local/etc/php/php.ini diff --git a/test/unit/cache/sfAPCCacheTest.php b/test/unit/cache/sfAPCCacheTest.php index dcfd902c3..203cb959b 100644 --- a/test/unit/cache/sfAPCCacheTest.php +++ b/test/unit/cache/sfAPCCacheTest.php @@ -37,4 +37,8 @@ $cache = new sfAPCCache(); $cache->initialize(); +// make sure expired keys are dropped +// see https://github.com/krakjoe/apcu/issues/391 +ini_set('apc.use_request_time', 0); + sfCacheDriverTests::launch($t, $cache); diff --git a/test/unit/cache/sfCacheDriverTests.class.php b/test/unit/cache/sfCacheDriverTests.class.php index 13e502f1c..60cb3e9ac 100644 --- a/test/unit/cache/sfCacheDriverTests.class.php +++ b/test/unit/cache/sfCacheDriverTests.class.php @@ -18,7 +18,8 @@ public static function launch($t, $cache) $t->is($cache->get('test'), $data, '->get() retrieves data form the cache'); $t->is($cache->has('test'), true, '->has() returns true if the cache exists'); - $t->ok($cache->set('test', $data, -10), '->set() takes a lifetime as its third argument'); + $t->ok($cache->set('test', $data, 1), '->set() takes a lifetime as its third argument'); + sleep(2); $t->is($cache->get('test', 'default'), 'default', '->get() returns the default value if cache has expired'); $t->is($cache->has('test'), false, '->has() returns true if the cache exists'); @@ -47,21 +48,24 @@ public static function launch($t, $cache) // ->clean() $t->diag('->clean()'); $data = 'some random data to store in the cache system...'; - $cache->set('foo', $data, -10); + $cache->set('foo', $data, 1); $cache->set('bar', $data, 86400); + sleep(2); $cache->clean(sfCache::OLD); $t->is($cache->has('foo'), false, '->clean() cleans old cache key if given the sfCache::OLD argument'); $t->is($cache->has('bar'), true, '->clean() cleans old cache key if given the sfCache::OLD argument'); - $cache->set('foo', $data, -10); + $cache->set('foo', $data, -1); + sleep(2); $cache->set('bar', $data, 86400); $cache->clean(sfCache::ALL); $t->is($cache->has('foo'), false, '->clean() cleans all cache key if given the sfCache::ALL argument'); $t->is($cache->has('bar'), false, '->clean() cleans all cache key if given the sfCache::ALL argument'); - $cache->set('foo', $data, -10); + $cache->set('foo', $data, 1); + sleep(2); $cache->set('bar', $data, 86400); $cache->clean(); @@ -126,7 +130,8 @@ public static function launch($t, $cache) $t->ok($delta >= $lifetime - 1 && $delta <= $lifetime, '->getTimeout() returns the timeout time for a given cache key'); } - $cache->set('bar', 'foo', -10); + $cache->set('bar', 'foo', 1); + sleep(2); $t->is($cache->getTimeout('bar'), 0, '->getTimeout() returns the timeout time for a given cache key'); foreach (array(86400, 10) as $lifetime) { @@ -148,7 +153,8 @@ public static function launch($t, $cache) $t->ok($lastModified >= time() - 1 && $lastModified <= time(), '->getLastModified() returns the last modified time for a given cache key'); } - $cache->set('bar', 'foo', -10); + $cache->set('bar', 'foo', 1); + sleep(2); $t->is($cache->getLastModified('bar'), 0, '->getLastModified() returns the last modified time for a given cache key'); foreach (array(86400, 10) as $lifetime) { From 234865da03b30094e4047be672d630762d771517 Mon Sep 17 00:00:00 2001 From: Paulo Magalhaes Date: Sun, 23 Jan 2022 23:05:09 +0000 Subject: [PATCH 2/4] Add APCu support Support for the APCu extension (through sfAPCuCache) as an alternative to APC, which no longer works with recent versions of PHP. --- .github/workflows/continuous-integration.yml | 2 + data/bin/check_configuration.php | 2 +- docker-compose.yml | 18 +- lib/autoload/sfCoreAutoload.class.php | 1 + lib/cache/sfAPCuCache.class.php | 195 +++++++++++++++++++ test/unit/cache/sfAPCCacheTest.php | 19 +- 6 files changed, 225 insertions(+), 12 deletions(-) create mode 100644 lib/cache/sfAPCuCache.class.php diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 561845967..47e723aed 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -39,6 +39,8 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: "${{ matrix.php-version }}" + extensions: apcu + ini-values: apc.enable_cli=1 - name: Get composer cache directory id: composer-cache diff --git a/data/bin/check_configuration.php b/data/bin/check_configuration.php index cdb3276e5..ccccb38ec 100644 --- a/data/bin/check_configuration.php +++ b/data/bin/check_configuration.php @@ -77,7 +77,7 @@ function get_ini_path() check(function_exists('posix_isatty'), 'The posix_isatty() is available', 'Install and enable the php_posix extension (used to colorized the CLI output)', false); $accelerator = - (function_exists('apc_store') && ini_get('apc.enabled')) + ((function_exists('apc_store') || function_exists('apcu_store')) && ini_get('apc.enabled')) || function_exists('eaccelerator_put') && ini_get('eaccelerator.enable') || function_exists('xcache_set'); check($accelerator, 'A PHP accelerator is installed', 'Install a PHP accelerator like APC (highly recommended)', false); diff --git a/docker-compose.yml b/docker-compose.yml index ce0823a88..b0f11aa46 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -94,7 +94,7 @@ services: args: PHP_TAG: '7.0-cli-jessie' MEMCACHE_VERSION: '4.0.5.2' - APCU_VERSION: '' + APCU_VERSION: '5.1.23' php71: <<: *services_php54 @@ -103,7 +103,7 @@ services: args: PHP_TAG: '7.1-cli-jessie' MEMCACHE_VERSION: '4.0.5.2' - APCU_VERSION: '' + APCU_VERSION: '5.1.23' php72: @@ -113,7 +113,7 @@ services: args: PHP_VERSION: '7.2' MEMCACHE_VERSION: '4.0.5.2' - APCU_VERSION: '' + APCU_VERSION: '5.1.23' php73: @@ -123,7 +123,7 @@ services: args: PHP_VERSION: '7.3' MEMCACHE_VERSION: '4.0.5.2' - APCU_VERSION: '' + APCU_VERSION: '5.1.23' php74: @@ -133,7 +133,7 @@ services: args: PHP_VERSION: '7.4' MEMCACHE_VERSION: '4.0.5.2' - APCU_VERSION: '' + APCU_VERSION: '5.1.23' php80: @@ -143,7 +143,7 @@ services: args: PHP_VERSION: '8.0' MEMCACHE_VERSION: '8.0' - APCU_VERSION: '' + APCU_VERSION: '5.1.23' php81: @@ -153,7 +153,7 @@ services: args: PHP_VERSION: '8.1' MEMCACHE_VERSION: '8.0' - APCU_VERSION: '' + APCU_VERSION: '5.1.23' php82: <<: *services_php54 @@ -162,7 +162,7 @@ services: args: PHP_VERSION: '8.2' MEMCACHE_VERSION: '8.0' - APCU_VERSION: '' + APCU_VERSION: '5.1.23' php83: <<: *services_php54 @@ -171,7 +171,7 @@ services: args: PHP_VERSION: '8.3' MEMCACHE_VERSION: '8.0' - APCU_VERSION: '' + APCU_VERSION: '5.1.23' db: diff --git a/lib/autoload/sfCoreAutoload.class.php b/lib/autoload/sfCoreAutoload.class.php index 2b711629c..fafe37026 100755 --- a/lib/autoload/sfCoreAutoload.class.php +++ b/lib/autoload/sfCoreAutoload.class.php @@ -44,6 +44,7 @@ class sfCoreAutoload 'sfcoreautoload' => 'autoload/sfCoreAutoload.class.php', 'sfsimpleautoload' => 'autoload/sfSimpleAutoload.class.php', 'sfapccache' => 'cache/sfAPCCache.class.php', + 'sfapcucache' => 'cache/sfAPCuCache.class.php', 'sfcache' => 'cache/sfCache.class.php', 'sfeacceleratorcache' => 'cache/sfEAcceleratorCache.class.php', 'sffilecache' => 'cache/sfFileCache.class.php', diff --git a/lib/cache/sfAPCuCache.class.php b/lib/cache/sfAPCuCache.class.php new file mode 100644 index 000000000..fe813331c --- /dev/null +++ b/lib/cache/sfAPCuCache.class.php @@ -0,0 +1,195 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Cache class that stores cached content in APCu. + * + * @author Fabien Potencier + * @author Paulo M + * + * @version SVN: $Id$ + */ +class sfAPCuCache extends sfCache +{ + protected $enabled; + + /** + * Initializes this sfCache instance. + * + * Available options: + * + * * see sfCache for options available for all drivers + * + * @see sfCache + */ + public function initialize($options = array()) + { + parent::initialize($options); + + $this->enabled = function_exists('apcu_store') && ini_get('apc.enabled'); + } + + /** + * @see sfCache + * + * @param mixed|null $default + */ + public function get($key, $default = null) + { + if (!$this->enabled) { + return $default; + } + + $value = $this->fetch($this->getOption('prefix').$key, $has); + + return $has ? $value : $default; + } + + /** + * @see sfCache + */ + public function has($key) + { + if (!$this->enabled) { + return false; + } + + $this->fetch($this->getOption('prefix').$key, $has); + + return $has; + } + + /** + * @see sfCache + * + * @param mixed|null $lifetime + */ + public function set($key, $data, $lifetime = null) + { + if (!$this->enabled) { + return true; + } + + return apcu_store($this->getOption('prefix').$key, $data, $this->getLifetime($lifetime)); + } + + /** + * @see sfCache + */ + public function remove($key) + { + if (!$this->enabled) { + return true; + } + + return apcu_delete($this->getOption('prefix').$key); + } + + /** + * @see sfCache + */ + public function clean($mode = sfCache::ALL) + { + if (!$this->enabled) { + return true; + } + + if (sfCache::ALL === $mode) { + return apcu_clear_cache(); + } + } + + /** + * @see sfCache + */ + public function getLastModified($key) + { + if ($info = $this->getCacheInfo($key)) { + return $info['creation_time'] + $info['ttl'] > time() ? $info['mtime'] : 0; + } + + return 0; + } + + /** + * @see sfCache + */ + public function getTimeout($key) + { + if ($info = $this->getCacheInfo($key)) { + return $info['creation_time'] + $info['ttl'] > time() ? $info['creation_time'] + $info['ttl'] : 0; + } + + return 0; + } + + /** + * @see sfCache + */ + public function removePattern($pattern) + { + if (!$this->enabled) { + return true; + } + + $infos = apcu_cache_info(); + if (!is_array($infos['cache_list'])) { + return; + } + + $regexp = self::patternToRegexp($this->getOption('prefix').$pattern); + + foreach ($infos['cache_list'] as $info) { + if (preg_match($regexp, $info['info'])) { + apcu_delete($info['info']); + } + } + } + + /** + * Gets the cache info. + * + * @param string $key The cache key + * + * @return string + */ + protected function getCacheInfo($key) + { + if (!$this->enabled) { + return false; + } + + $infos = apcu_cache_info(); + + if (is_array($infos['cache_list'])) { + foreach ($infos['cache_list'] as $info) { + if ($this->getOption('prefix').$key == $info['info']) { + return $info; + } + } + } + + return null; + } + + private function fetch($key, &$success) + { + $has = null; + $value = apcu_fetch($key, $has); + // the second argument was added in APC 3.0.17. If it is still null we fall back to the value returned + if (null !== $has) { + $success = $has; + } else { + $success = false !== $value; + } + + return $value; + } +} diff --git a/test/unit/cache/sfAPCCacheTest.php b/test/unit/cache/sfAPCCacheTest.php index 203cb959b..32f7b6bba 100644 --- a/test/unit/cache/sfAPCCacheTest.php +++ b/test/unit/cache/sfAPCCacheTest.php @@ -13,8 +13,23 @@ $plan = 64; $t = new lime_test($plan); +if (extension_loaded('apc')) { + $cacheClass = 'sfAPCCache'; +} elseif (extension_loaded('apcu')) { + if ('5.1.22' === phpversion('apcu')) { + $t->skip('APCu 5.1.22 has a regression and shouldn\'t be used', $plan); + + return; + } + $cacheClass = 'sfAPCuCache'; +} else { + $t->skip('APC or APCu must be loaded to run these tests', $plan); + + return; +} + try { - new sfAPCCache(); + new $cacheClass(); } catch (sfInitializationException $e) { $t->skip($e->getMessage(), $plan); @@ -34,7 +49,7 @@ // ->initialize() $t->diag('->initialize()'); -$cache = new sfAPCCache(); +$cache = new $cacheClass(); $cache->initialize(); // make sure expired keys are dropped From 30966ee7fc6915ac29930a176a204825f76409ae Mon Sep 17 00:00:00 2001 From: Paulo Magalhaes Date: Thu, 10 Feb 2022 20:20:18 +0000 Subject: [PATCH 3/4] Fix sfCacheSessionStorageTest with PHP>=7.2 From PHP 7.2 onward, session functions are stricter and may not work if output/headers have already been sent out. Using output buffering prevents this issue. --- test/unit/storage/sfCacheSessionStorageTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/storage/sfCacheSessionStorageTest.php b/test/unit/storage/sfCacheSessionStorageTest.php index 7f8cd7298..ba54439ce 100644 --- a/test/unit/storage/sfCacheSessionStorageTest.php +++ b/test/unit/storage/sfCacheSessionStorageTest.php @@ -12,6 +12,8 @@ require_once __DIR__.'/../../bootstrap/functional.php'; +ob_start(); + $_test_dir = realpath(__DIR__.'/../../'); require_once $_test_dir.'/../lib/vendor/lime/lime.php'; From 058149b16fabbd962ddc7337cb05e8cde00700bc Mon Sep 17 00:00:00 2001 From: Paulo Magalhaes Date: Thu, 10 Feb 2022 20:29:22 +0000 Subject: [PATCH 4/4] Remove test dependency on APC Replace the use of sfAPCCache with sfFileCache in sfCacheSessionStorageTest so that it doesn't depend on APC being available. --- test/unit/storage/sfCacheSessionStorageTest.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/unit/storage/sfCacheSessionStorageTest.php b/test/unit/storage/sfCacheSessionStorageTest.php index ba54439ce..2e2329b11 100644 --- a/test/unit/storage/sfCacheSessionStorageTest.php +++ b/test/unit/storage/sfCacheSessionStorageTest.php @@ -20,15 +20,14 @@ sfConfig::set('sf_symfony_lib_dir', realpath($_test_dir.'/../lib')); +// setup cache +$temp = tempnam('/tmp/cache_dir', 'tmp'); +unlink($temp); +mkdir($temp); + $plan = 8; $t = new lime_test($plan); -if (!ini_get('apc.enable_cli')) { - $t->skip('APC must be enable on CLI to run these tests', $plan); - - return; -} - // initialize the storage try { $storage = new sfCacheSessionStorage(); @@ -37,7 +36,7 @@ $t->pass('->__construct() throws an exception when not provided a cache option'); } -$storage = new sfCacheSessionStorage(array('cache' => array('class' => 'sfAPCCache', 'param' => array()))); +$storage = new sfCacheSessionStorage(array('cache' => array('class' => 'sfFileCache', 'param' => array('cache_dir' => $temp)))); $t->ok($storage instanceof sfStorage, '->__construct() is an instance of sfStorage'); $storage->write('test', 123); @@ -63,3 +62,7 @@ // shutdown the storage $storage->shutdown(); + +// clean up cache +sfToolkit::clearDirectory($temp); +rmdir($temp);