Skip to content

Commit

Permalink
feature #18894 [Cache] Added PhpFilesAdapter (trakos, nicolas-grekas)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
nicolas-grekas committed Jun 6, 2016
2 parents 37c9c39 + 8983e83 commit 6f83328
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 79 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions appveyor.yml
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/Symfony/Component/Cache/Adapter/AbstractAdapter.php
Expand Up @@ -70,6 +70,15 @@ function ($deferred, $namespace, &$expiredIds) {

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);
Expand Down
82 changes: 3 additions & 79 deletions src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php
Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -91,68 +65,18 @@ protected function doHave($id)
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}
*/
protected function doSave(array $values, $lifetime)
{
$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);
}
}
110 changes: 110 additions & 0 deletions src/Symfony/Component/Cache/Adapter/FilesystemAdapterTrait.php
@@ -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);
}
}
123 changes: 123 additions & 0 deletions src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php
@@ -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;
}
}

0 comments on commit 6f83328

Please sign in to comment.