feature #18894 [Cache] Added PhpFilesAdapter (trakos, nicolas-grekas)

This PR was merged into the 3.2-dev branch.

Discussion
----------

[Cache] Added PhpFilesAdapter

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

This is taking over #18832.
With a warm cache I get these numbers consistently (PhpArrayAdapter being the implem in #18823 ):
```
Fetching randomly 5000 items 10000 times:

 Symfony\Component\Cache\Adapter\FilesystemAdapter: 0.1367,    2 megabytes
   Symfony\Component\Cache\Adapter\PhpArrayAdapter: 0.0071,    2 megabytes
   Symfony\Component\Cache\Adapter\PhpFilesAdapter: 0.0389,    2 megabytes
       Symfony\Component\Cache\Adapter\ApcuAdapter: 0.0361,    2 megabytes
```

This means that the PhpArrayAdapter should be used first, then ApcuAdapter preferred over PhpFilesAdapter, then FilesystemAdapter. This is what AbstractAdapter does here.

Also note that to get the cache working, one should stay within the limits defined by the following ini settings:
- memory_limit
- apc.shm_size
- opcache.memory_consumption
- opcache.interned_strings_buffer
- opcache.max_accelerated_files

Commits
-------

8983e83 [Cache] Optimize & wire PhpFilesAdapter
14bcd79 [Cache] Added PhpFilesAdapter
This commit is contained in:
Nicolas Grekas 2016-06-06 10:10:42 +02:00
commit 6f833284cb
7 changed files with 286 additions and 79 deletions

View File

@ -52,6 +52,7 @@ before_install:
- if [[ ! $PHP = hhvm* ]]; then INI_FILE=~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; else INI_FILE=/etc/hhvm/php.ini; fi
- if [[ ! $skip ]]; then echo memory_limit = -1 >> $INI_FILE; fi
- if [[ ! $skip ]]; then echo session.gc_probability = 0 >> $INI_FILE; fi
- if [[ ! $skip ]]; then echo opcache.enable_cli = 1 >> $INI_FILE; fi
- if [[ ! $skip && $PHP = 5.* ]]; then echo extension = mongo.so >> $INI_FILE; fi
- if [[ ! $skip && $PHP = 5.* ]]; then echo extension = memcache.so >> $INI_FILE; fi
- if [[ ! $skip && $PHP = 5.* ]]; then (echo yes | pecl install -f apcu-4.0.10 && echo apc.enable_cli = 1 >> $INI_FILE); fi

View File

@ -35,6 +35,8 @@ install:
- IF %PHP%==1 echo date.timezone="UTC" >> php.ini-min
- IF %PHP%==1 echo extension_dir=ext >> php.ini-min
- IF %PHP%==1 copy /Y php.ini-min php.ini-max
- IF %PHP%==1 echo zend_extension=php_opcache.dll >> php.ini-max
- IF %PHP%==1 echo opcache.enable_cli=1 >> php.ini-max
- IF %PHP%==1 echo extension=php_openssl.dll >> php.ini-max
- IF %PHP%==1 echo extension=php_apcu.dll >> php.ini-max
- IF %PHP%==1 echo apc.enable_cli=1 >> php.ini-max

View File

@ -70,6 +70,15 @@ abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface
public static function createSystemCache($namespace, $defaultLifetime, $version, $directory, LoggerInterface $logger = null)
{
if (!ApcuAdapter::isSupported() && PhpFilesAdapter::isSupported()) {
$opcache = new PhpFilesAdapter($namespace, $defaultLifetime, $directory);
if (null !== $logger) {
$opcache->setLogger($logger);
}
return $opcache;
}
$fs = new FilesystemAdapter($namespace, $defaultLifetime, $directory);
if (null !== $logger) {
$fs->setLogger($logger);

View File

@ -11,43 +11,17 @@
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class FilesystemAdapter extends AbstractAdapter
{
private $directory;
use FilesystemAdapterTrait;
public function __construct($namespace = '', $defaultLifetime = 0, $directory = null)
{
parent::__construct('', $defaultLifetime);
if (!isset($directory[0])) {
$directory = sys_get_temp_dir().'/symfony-cache';
}
if (isset($namespace[0])) {
if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
throw new InvalidArgumentException(sprintf('FilesystemAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
}
$directory .= '/'.$namespace;
}
if (!file_exists($dir = $directory.'/.')) {
@mkdir($directory, 0777, true);
}
if (false === $dir = realpath($dir)) {
throw new InvalidArgumentException(sprintf('Cache directory does not exist (%s)', $directory));
}
if (!is_writable($dir .= DIRECTORY_SEPARATOR)) {
throw new InvalidArgumentException(sprintf('Cache directory is not writable (%s)', $directory));
}
// On Windows the whole path is limited to 258 chars
if ('\\' === DIRECTORY_SEPARATOR && strlen($dir) > 234) {
throw new InvalidArgumentException(sprintf('Cache directory too long (%s)', $directory));
}
$this->directory = $dir;
$this->init($namespace, $directory);
}
/**
@ -91,35 +65,6 @@ class FilesystemAdapter extends AbstractAdapter
return file_exists($file) && (@filemtime($file) > time() || $this->doFetch(array($id)));
}
/**
* {@inheritdoc}
*/
protected function doClear($namespace)
{
$ok = true;
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS)) as $file) {
$ok = ($file->isDir() || @unlink($file) || !file_exists($file)) && $ok;
}
return $ok;
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids)
{
$ok = true;
foreach ($ids as $id) {
$file = $this->getFile($id);
$ok = (!file_exists($file) || @unlink($file) || !file_exists($file)) && $ok;
}
return $ok;
}
/**
* {@inheritdoc}
*/
@ -127,32 +72,11 @@ class FilesystemAdapter extends AbstractAdapter
{
$ok = true;
$expiresAt = $lifetime ? time() + $lifetime : PHP_INT_MAX;
$tmp = $this->directory.uniqid('', true);
foreach ($values as $id => $value) {
$file = $this->getFile($id, true);
$value = $expiresAt."\n".rawurlencode($id)."\n".serialize($value);
if (false !== @file_put_contents($tmp, $value)) {
@touch($tmp, $expiresAt);
$ok = @rename($tmp, $file) && $ok;
} else {
$ok = false;
}
$ok = $this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".serialize($value), $expiresAt) && $ok;
}
return $ok;
}
private function getFile($id, $mkdir = false)
{
$hash = str_replace('/', '-', base64_encode(md5($id, true)));
$dir = $this->directory.$hash[0].DIRECTORY_SEPARATOR.$hash[1].DIRECTORY_SEPARATOR;
if ($mkdir && !file_exists($dir)) {
@mkdir($dir, 0777, true);
}
return $dir.substr($hash, 2, -2);
}
}

View File

@ -0,0 +1,110 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
trait FilesystemAdapterTrait
{
private $directory;
private $tmp;
private function init($namespace, $directory)
{
if (!isset($directory[0])) {
$directory = sys_get_temp_dir().'/symfony-cache';
}
if (isset($namespace[0])) {
if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
}
$directory .= '/'.$namespace;
}
if (!file_exists($dir = $directory.'/.')) {
@mkdir($directory, 0777, true);
}
if (false === $dir = realpath($dir)) {
throw new InvalidArgumentException(sprintf('Cache directory does not exist (%s)', $directory));
}
if (!is_writable($dir .= DIRECTORY_SEPARATOR)) {
throw new InvalidArgumentException(sprintf('Cache directory is not writable (%s)', $directory));
}
// On Windows the whole path is limited to 258 chars
if ('\\' === DIRECTORY_SEPARATOR && strlen($dir) > 234) {
throw new InvalidArgumentException(sprintf('Cache directory too long (%s)', $directory));
}
$this->directory = $dir;
$this->tmp = $this->directory.uniqid('', true);
}
/**
* {@inheritdoc}
*/
protected function doClear($namespace)
{
$ok = true;
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS)) as $file) {
$ok = ($file->isDir() || @unlink($file) || !file_exists($file)) && $ok;
}
return $ok;
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids)
{
$ok = true;
foreach ($ids as $id) {
$file = $this->getFile($id);
$ok = (!file_exists($file) || @unlink($file) || !file_exists($file)) && $ok;
}
return $ok;
}
private function write($file, $data, $expiresAt = null)
{
if (false === @file_put_contents($this->tmp, $data)) {
return false;
}
if (null !== $expiresAt) {
@touch($this->tmp, $expiresAt);
}
if (@rename($this->tmp, $file)) {
return true;
}
@unlink($this->tmp);
return false;
}
private function getFile($id, $mkdir = false)
{
$hash = str_replace('/', '-', base64_encode(md5(static::class.$id, true)));
$dir = $this->directory.$hash[0].DIRECTORY_SEPARATOR.$hash[1].DIRECTORY_SEPARATOR;
if ($mkdir && !file_exists($dir)) {
@mkdir($dir, 0777, true);
}
return $dir.substr($hash, 2, -2);
}
}

View File

@ -0,0 +1,123 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
/**
* @author Piotr Stankowski <git@trakos.pl>
* @author Nicolas Grekas <p@tchwork.com>
*/
class PhpFilesAdapter extends AbstractAdapter
{
use FilesystemAdapterTrait;
private $includeHandler;
public static function isSupported()
{
return function_exists('opcache_compile_file') && ini_get('opcache.enable');
}
public function __construct($namespace = '', $defaultLifetime = 0, $directory = null)
{
if (!static::isSupported()) {
throw new CacheException('OPcache is not enabled');
}
parent::__construct('', $defaultLifetime);
$this->init($namespace, $directory);
$e = new \Exception();
$this->includeHandler = function () use ($e) { throw $e; };
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids)
{
$values = array();
$now = time();
set_error_handler($this->includeHandler);
try {
foreach ($ids as $id) {
try {
$file = $this->getFile($id);
list($expiresAt, $values[$id]) = include $file;
if ($now >= $expiresAt) {
unset($values[$id]);
}
} catch (\Exception $e) {
continue;
}
}
} finally {
restore_error_handler();
}
foreach ($values as $id => $value) {
if ('N;' === $value) {
$values[$id] = null;
} elseif (is_string($value) && isset($value[2]) && ':' === $value[1]) {
$values[$id] = unserialize($value);
}
}
return $values;
}
/**
* {@inheritdoc}
*/
protected function doHave($id)
{
return (bool) $this->doFetch(array($id));
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, $lifetime)
{
$ok = true;
$data = array($lifetime ? time() + $lifetime : PHP_INT_MAX, '');
foreach ($values as $id => $value) {
if (null === $value || is_object($value)) {
$value = serialize($value);
} elseif (is_array($value)) {
$serialized = serialize($value);
$unserialized = unserialize($serialized);
// Store arrays serialized if they contain any objects or references
if ($unserialized !== $value || (false !== strpos($serialized, ';R:') && preg_match('/;R:[1-9]/', $serialized))) {
$value = $serialized;
}
} elseif (is_string($value)) {
// Serialize strings if they could be confused with serialized objects or arrays
if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) {
$value = serialize($value);
}
} elseif (!is_scalar($value)) {
throw new InvalidArgumentException(sprintf('Value of type "%s" is not serializable', $key, gettype($value)));
}
$data[1] = $value;
$file = $this->getFile($id, true);
$ok = $this->write($file, '<?php return '.var_export($data, true).';') && $ok;
@opcache_compile_file($file);
}
return $ok;
}
}

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Tests\Adapter;
use Cache\IntegrationTests\CachePoolTest;
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
/**
* @group time-sensitive
*/
class PhpFilesAdapterTest extends CachePoolTest
{
public function createCachePool()
{
if (defined('HHVM_VERSION')) {
$this->skippedTests['testDeferredSaveWithoutCommit'] = 'Fails on HHVM';
}
if (!PhpFilesAdapter::isSupported()) {
$this->markTestSkipped('OPcache extension is not enabled.');
}
return new PhpFilesAdapter('sf-cache');
}
public static function tearDownAfterClass()
{
FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache');
}
}