From 48a5d5e8a9188cfb92d980c79e4f0c07d7e9193f Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 16 Jan 2020 14:19:26 +0100 Subject: [PATCH] [Cache] Add LRU + max-lifetime capabilities to ArrayCache --- .../Component/Cache/Adapter/ArrayAdapter.php | 87 +++++++++++++++++-- src/Symfony/Component/Cache/CHANGELOG.md | 5 ++ .../Cache/Tests/Adapter/ArrayAdapterTest.php | 32 +++++++ 3 files changed, 115 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php index 05920d01a9..ffec7d3d2c 100644 --- a/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ArrayAdapter.php @@ -15,10 +15,15 @@ use Psr\Cache\CacheItemInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; use Symfony\Component\Cache\ResettableInterface; use Symfony\Contracts\Cache\CacheInterface; /** + * An in-memory cache storage. + * + * Acts as a least-recently-used (LRU) storage when configured with a maximum number of items. + * * @author Nicolas Grekas */ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface @@ -29,13 +34,25 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter private $values = []; private $expiries = []; private $createCacheItem; + private $maxLifetime; + private $maxItems; /** * @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise */ - public function __construct(int $defaultLifetime = 0, bool $storeSerialized = true) + public function __construct(int $defaultLifetime = 0, bool $storeSerialized = true, int $maxLifetime = 0, int $maxItems = 0) { + if (0 > $maxLifetime) { + throw new InvalidArgumentException(sprintf('Argument $maxLifetime must be a positive integer, %d passed.', $maxLifetime)); + } + + if (0 > $maxItems) { + throw new InvalidArgumentException(sprintf('Argument $maxItems must be a positive integer, %d passed.', $maxItems)); + } + $this->storeSerialized = $storeSerialized; + $this->maxLifetime = $maxLifetime; + $this->maxItems = $maxItems; $this->createCacheItem = \Closure::bind( static function ($key, $value, $isHit) use ($defaultLifetime) { $item = new CacheItem(); @@ -84,6 +101,13 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter public function hasItem($key) { if (\is_string($key) && isset($this->expiries[$key]) && $this->expiries[$key] > microtime(true)) { + if ($this->maxItems) { + // Move the item last in the storage + $value = $this->values[$key]; + unset($this->values[$key]); + $this->values[$key] = $value; + } + return true; } CacheItem::validateKey($key); @@ -97,7 +121,12 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter public function getItem($key) { if (!$isHit = $this->hasItem($key)) { - $this->values[$key] = $value = null; + $value = null; + + if (!$this->maxItems) { + // Track misses in non-LRU mode only + $this->values[$key] = null; + } } else { $value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key]; } @@ -164,7 +193,9 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter $value = $item["\0*\0value"]; $expiry = $item["\0*\0expiry"]; - if (null !== $expiry && $expiry <= microtime(true)) { + $now = microtime(true); + + if (null !== $expiry && $expiry <= $now) { $this->deleteItem($key); return true; @@ -173,7 +204,23 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter return false; } if (null === $expiry && 0 < $item["\0*\0defaultLifetime"]) { - $expiry = microtime(true) + $item["\0*\0defaultLifetime"]; + $expiry = $item["\0*\0defaultLifetime"]; + $expiry = $now + ($expiry > ($this->maxLifetime ?: $expiry) ? $this->maxLifetime : $expiry); + } elseif ($this->maxLifetime && (null === $expiry || $expiry > $now + $this->maxLifetime)) { + $expiry = $now + $this->maxLifetime; + } + + if ($this->maxItems) { + unset($this->values[$key]); + + // Iterate items and vacuum expired ones while we are at it + foreach ($this->values as $k => $v) { + if ($this->expiries[$k] > $now && \count($this->values) < $this->maxItems) { + break; + } + + unset($this->values[$k], $this->expiries[$k]); + } } $this->values[$key] = $value; @@ -210,15 +257,21 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter public function clear(string $prefix = '') { if ('' !== $prefix) { + $now = microtime(true); + foreach ($this->values as $key => $value) { - if (0 === strpos($key, $prefix)) { + if (!isset($this->expiries[$key]) || $this->expiries[$key] <= $now || 0 === strpos($key, $prefix)) { unset($this->values[$key], $this->expiries[$key]); } } - } else { - $this->values = $this->expiries = []; + + if ($this->values) { + return true; + } } + $this->values = $this->expiries = []; + return true; } @@ -258,8 +311,20 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter { foreach ($keys as $i => $key) { if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] > $now || !$this->deleteItem($key))) { - $this->values[$key] = $value = null; + $value = null; + + if (!$this->maxItems) { + // Track misses in non-LRU mode only + $this->values[$key] = null; + } } else { + if ($this->maxItems) { + // Move the item last in the storage + $value = $this->values[$key]; + unset($this->values[$key]); + $this->values[$key] = $value; + } + $value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key]; } unset($keys[$i]); @@ -314,8 +379,12 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter $value = false; } if (false === $value) { - $this->values[$key] = $value = null; + $value = null; $isHit = false; + + if (!$this->maxItems) { + $this->values[$key] = null; + } } } diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index 2f2b2493ee..c7ed54ac91 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.1.0 +----- + + * added max-items + LRU + max-lifetime capabilities to `ArrayCache` + 5.0.0 ----- diff --git a/src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php index ff37479cc1..f23ca6b806 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/ArrayAdapterTest.php @@ -55,4 +55,36 @@ class ArrayAdapterTest extends AdapterTestCase $this->assertArrayHasKey('bar', $values); $this->assertNull($values['bar']); } + + public function testMaxLifetime() + { + $cache = new ArrayAdapter(0, false, 1); + + $item = $cache->getItem('foo'); + $item->expiresAfter(2); + $cache->save($item->set(123)); + + $this->assertTrue($cache->hasItem('foo')); + sleep(1); + $this->assertFalse($cache->hasItem('foo')); + } + + public function testMaxItems() + { + $cache = new ArrayAdapter(0, false, 0, 2); + + $cache->save($cache->getItem('foo')); + $cache->save($cache->getItem('bar')); + $cache->save($cache->getItem('buz')); + + $this->assertFalse($cache->hasItem('foo')); + $this->assertTrue($cache->hasItem('bar')); + $this->assertTrue($cache->hasItem('buz')); + + $cache->save($cache->getItem('foo')); + + $this->assertFalse($cache->hasItem('bar')); + $this->assertTrue($cache->hasItem('buz')); + $this->assertTrue($cache->hasItem('foo')); + } }