add (pdo|chain) cache (adapter|simple) prune method

This commit is contained in:
Rob Frawley 2nd 2017-07-20 13:13:39 -04:00
parent 7695112601
commit b20a2376b2
No known key found for this signature in database
GPG Key ID: 02B8697CE6AA381A
16 changed files with 284 additions and 9 deletions

View File

@ -15,6 +15,7 @@ use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\PruneableInterface;
/**
* Chains several adapters together.
@ -24,7 +25,7 @@ use Symfony\Component\Cache\Exception\InvalidArgumentException;
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class ChainAdapter implements AdapterInterface
class ChainAdapter implements AdapterInterface, PruneableInterface
{
private $adapters = array();
private $adapterCount;
@ -231,4 +232,20 @@ class ChainAdapter implements AdapterInterface
return $committed;
}
/**
* {@inheritdoc}
*/
public function prune()
{
$pruned = true;
foreach ($this->adapters as $adapter) {
if ($adapter instanceof PruneableInterface) {
$pruned = $adapter->prune() && $pruned;
}
}
return $pruned;
}
}

View File

@ -11,9 +11,10 @@
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\PdoTrait;
class PdoAdapter extends AbstractAdapter
class PdoAdapter extends AbstractAdapter implements PruneableInterface
{
use PdoTrait;

View File

@ -5,9 +5,9 @@ CHANGELOG
-----
* added PruneableInterface so PSR-6 or PSR-16 cache implementations can declare support for manual stale cache pruning
* added FilesystemTrait::prune() and PhpFilesTrait::prune() implementations
* now FilesystemAdapter, PhpFilesAdapter, FilesystemCache, and PhpFilesCache implement PruneableInterface and support
manual stale cache pruning
* added prune logic to FilesystemTrait, PhpFilesTrait, PdoTrait, and ChainTrait
* now FilesystemAdapter, PhpFilesAdapter, FilesystemCache, PhpFilesCache, PdoAdapter, PdoCache, ChainAdapter, and
ChainCache implement PruneableInterface and support manual stale cache pruning
3.3.0
-----

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\Cache;
/**
* Interface for adapters and simple cache implementations that allow pruning expired items.
* Interface extends psr-6 and psr-16 caches to allow for pruning (deletion) of all expired cache items.
*/
interface PruneableInterface
{

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Cache\Simple;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\PruneableInterface;
/**
* Chains several caches together.
@ -22,7 +23,7 @@ use Symfony\Component\Cache\Exception\InvalidArgumentException;
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class ChainCache implements CacheInterface
class ChainCache implements CacheInterface, PruneableInterface
{
private $miss;
private $caches = array();
@ -219,4 +220,20 @@ class ChainCache implements CacheInterface
return $saved;
}
/**
* {@inheritdoc}
*/
public function prune()
{
$pruned = true;
foreach ($this->caches as $cache) {
if ($cache instanceof PruneableInterface) {
$pruned = $cache->prune() && $pruned;
}
}
return $pruned;
}
}

View File

@ -11,9 +11,10 @@
namespace Symfony\Component\Cache\Simple;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\PdoTrait;
class PdoCache extends AbstractCache
class PdoCache extends AbstractCache implements PruneableInterface
{
use PdoTrait;

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Cache\Tests\Adapter;
use Cache\IntegrationTests\CachePoolTest;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\PruneableInterface;
abstract class AdapterTestCase extends CachePoolTest
@ -83,6 +84,7 @@ abstract class AdapterTestCase extends CachePoolTest
$this->fail('Test classes for pruneable caches must implement `isPruned($cache, $name)` method.');
}
/** @var PruneableInterface|CacheItemPoolInterface $cache */
$cache = $this->createCachePool();
$doSet = function ($name, $value, \DateInterval $expiresAfter = null) use ($cache) {
@ -96,6 +98,18 @@ abstract class AdapterTestCase extends CachePoolTest
$cache->save($item);
};
$doSet('foo', 'foo-val', new \DateInterval('PT05S'));
$doSet('bar', 'bar-val', new \DateInterval('PT10S'));
$doSet('baz', 'baz-val', new \DateInterval('PT15S'));
$doSet('qux', 'qux-val', new \DateInterval('PT20S'));
sleep(30);
$cache->prune();
$this->assertTrue($this->isPruned($cache, 'foo'));
$this->assertTrue($this->isPruned($cache, 'bar'));
$this->assertTrue($this->isPruned($cache, 'baz'));
$this->assertTrue($this->isPruned($cache, 'qux'));
$doSet('foo', 'foo-val');
$doSet('bar', 'bar-val', new \DateInterval('PT20S'));
$doSet('baz', 'baz-val', new \DateInterval('PT40S'));

View File

@ -11,9 +11,11 @@
namespace Symfony\Component\Cache\Tests\Adapter;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\ChainAdapter;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Tests\Fixtures\ExternalAdapter;
/**
@ -44,4 +46,73 @@ class ChainAdapterTest extends AdapterTestCase
{
new ChainAdapter(array(new \stdClass()));
}
public function testPrune()
{
if (isset($this->skippedTests[__FUNCTION__])) {
$this->markTestSkipped($this->skippedTests[__FUNCTION__]);
}
$cache = new ChainAdapter(array(
$this->getPruneableMock(),
$this->getNonPruneableMock(),
$this->getPruneableMock(),
));
$this->assertTrue($cache->prune());
$cache = new ChainAdapter(array(
$this->getPruneableMock(),
$this->getFailingPruneableMock(),
$this->getPruneableMock(),
));
$this->assertFalse($cache->prune());
}
/**
* @return \PHPUnit_Framework_MockObject_MockObject|PruneableCacheInterface
*/
private function getPruneableMock()
{
$pruneable = $this
->getMockBuilder(PruneableCacheInterface::class)
->getMock();
$pruneable
->expects($this->atLeastOnce())
->method('prune')
->will($this->returnValue(true));
return $pruneable;
}
/**
* @return \PHPUnit_Framework_MockObject_MockObject|PruneableCacheInterface
*/
private function getFailingPruneableMock()
{
$pruneable = $this
->getMockBuilder(PruneableCacheInterface::class)
->getMock();
$pruneable
->expects($this->atLeastOnce())
->method('prune')
->will($this->returnValue(false));
return $pruneable;
}
/**
* @return \PHPUnit_Framework_MockObject_MockObject|AdapterInterface
*/
private function getNonPruneableMock()
{
return $this
->getMockBuilder(AdapterInterface::class)
->getMock();
}
}
interface PruneableCacheInterface extends PruneableInterface, AdapterInterface
{
}

View File

@ -12,12 +12,15 @@
namespace Symfony\Component\Cache\Tests\Adapter;
use Symfony\Component\Cache\Adapter\PdoAdapter;
use Symfony\Component\Cache\Tests\Traits\PdoPruneableTrait;
/**
* @group time-sensitive
*/
class PdoAdapterTest extends AdapterTestCase
{
use PdoPruneableTrait;
protected static $dbFile;
public static function setupBeforeClass()

View File

@ -13,12 +13,15 @@ namespace Symfony\Component\Cache\Tests\Adapter;
use Doctrine\DBAL\DriverManager;
use Symfony\Component\Cache\Adapter\PdoAdapter;
use Symfony\Component\Cache\Tests\Traits\PdoPruneableTrait;
/**
* @group time-sensitive
*/
class PdoDbalAdapterTest extends AdapterTestCase
{
use PdoPruneableTrait;
protected static $dbFile;
public static function setupBeforeClass()

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Cache\Tests\Simple;
use Cache\IntegrationTests\SimpleCacheTest;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Cache\PruneableInterface;
abstract class CacheTestCase extends SimpleCacheTest
@ -80,8 +81,21 @@ abstract class CacheTestCase extends SimpleCacheTest
$this->fail('Test classes for pruneable caches must implement `isPruned($cache, $name)` method.');
}
/** @var PruneableInterface|CacheInterface $cache */
$cache = $this->createSimpleCache();
$cache->set('foo', 'foo-val', new \DateInterval('PT05S'));
$cache->set('bar', 'bar-val', new \DateInterval('PT10S'));
$cache->set('baz', 'baz-val', new \DateInterval('PT15S'));
$cache->set('qux', 'qux-val', new \DateInterval('PT20S'));
sleep(30);
$cache->prune();
$this->assertTrue($this->isPruned($cache, 'foo'));
$this->assertTrue($this->isPruned($cache, 'bar'));
$this->assertTrue($this->isPruned($cache, 'baz'));
$this->assertTrue($this->isPruned($cache, 'qux'));
$cache->set('foo', 'foo-val');
$cache->set('bar', 'bar-val', new \DateInterval('PT20S'));
$cache->set('baz', 'baz-val', new \DateInterval('PT40S'));

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Cache\Tests\Simple;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Simple\ArrayCache;
use Symfony\Component\Cache\Simple\ChainCache;
use Symfony\Component\Cache\Simple\FilesystemCache;
@ -40,6 +42,75 @@ class ChainCacheTest extends CacheTestCase
*/
public function testInvalidCacheException()
{
new Chaincache(array(new \stdClass()));
new ChainCache(array(new \stdClass()));
}
public function testPrune()
{
if (isset($this->skippedTests[__FUNCTION__])) {
$this->markTestSkipped($this->skippedTests[__FUNCTION__]);
}
$cache = new ChainCache(array(
$this->getPruneableMock(),
$this->getNonPruneableMock(),
$this->getPruneableMock(),
));
$this->assertTrue($cache->prune());
$cache = new ChainCache(array(
$this->getPruneableMock(),
$this->getFailingPruneableMock(),
$this->getPruneableMock(),
));
$this->assertFalse($cache->prune());
}
/**
* @return \PHPUnit_Framework_MockObject_MockObject|PruneableCacheInterface
*/
private function getPruneableMock()
{
$pruneable = $this
->getMockBuilder(PruneableCacheInterface::class)
->getMock();
$pruneable
->expects($this->atLeastOnce())
->method('prune')
->will($this->returnValue(true));
return $pruneable;
}
/**
* @return \PHPUnit_Framework_MockObject_MockObject|PruneableCacheInterface
*/
private function getFailingPruneableMock()
{
$pruneable = $this
->getMockBuilder(PruneableCacheInterface::class)
->getMock();
$pruneable
->expects($this->atLeastOnce())
->method('prune')
->will($this->returnValue(false));
return $pruneable;
}
/**
* @return \PHPUnit_Framework_MockObject_MockObject|CacheInterface
*/
private function getNonPruneableMock()
{
return $this
->getMockBuilder(CacheInterface::class)
->getMock();
}
}
interface PruneableCacheInterface extends PruneableInterface, CacheInterface
{
}

View File

@ -12,12 +12,15 @@
namespace Symfony\Component\Cache\Tests\Simple;
use Symfony\Component\Cache\Simple\PdoCache;
use Symfony\Component\Cache\Tests\Traits\PdoPruneableTrait;
/**
* @group time-sensitive
*/
class PdoCacheTest extends CacheTestCase
{
use PdoPruneableTrait;
protected static $dbFile;
public static function setupBeforeClass()

View File

@ -13,12 +13,15 @@ namespace Symfony\Component\Cache\Tests\Simple;
use Doctrine\DBAL\DriverManager;
use Symfony\Component\Cache\Simple\PdoCache;
use Symfony\Component\Cache\Tests\Traits\PdoPruneableTrait;
/**
* @group time-sensitive
*/
class PdoDbalCacheTest extends CacheTestCase
{
use PdoPruneableTrait;
protected static $dbFile;
public static function setupBeforeClass()

View File

@ -0,0 +1,34 @@
<?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\Traits;
trait PdoPruneableTrait
{
protected function isPruned($cache, $name)
{
$o = new \ReflectionObject($cache);
if (!$o->hasMethod('getConnection')) {
self::fail('Cache does not have "getConnection()" method.');
}
$getPdoConn = $o->getMethod('getConnection');
$getPdoConn->setAccessible(true);
/** @var \Doctrine\DBAL\Statement $select */
$select = $getPdoConn->invoke($cache)->prepare('SELECT 1 FROM cache_items WHERE item_id LIKE :id');
$select->bindValue(':id', sprintf('%%%s', $name));
$select->execute();
return 0 === count($select->fetchAll(\PDO::FETCH_COLUMN));
}
}

View File

@ -34,6 +34,7 @@ trait PdoTrait
private $username = '';
private $password = '';
private $connectionOptions = array();
private $namespace;
private function init($connOrDsn, $namespace, $defaultLifetime, array $options)
{
@ -63,6 +64,7 @@ trait PdoTrait
$this->username = isset($options['db_username']) ? $options['db_username'] : $this->username;
$this->password = isset($options['db_password']) ? $options['db_password'] : $this->password;
$this->connectionOptions = isset($options['db_connection_options']) ? $options['db_connection_options'] : $this->connectionOptions;
$this->namespace = $namespace;
parent::__construct($namespace, $defaultLifetime);
}
@ -137,6 +139,27 @@ trait PdoTrait
$conn->exec($sql);
}
/**
* {@inheritdoc}
*/
public function prune()
{
$deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= :time";
if ('' !== $this->namespace) {
$deleteSql .= " AND $this->idCol LIKE :namespace";
}
$delete = $this->getConnection()->prepare($deleteSql);
$delete->bindValue(':time', time(), \PDO::PARAM_INT);
if ('' !== $this->namespace) {
$delete->bindValue(':namespace', sprintf('%s%%', $this->namespace), \PDO::PARAM_STR);
}
return $delete->execute();
}
/**
* {@inheritdoc}
*/