feature #27009 [Cache] Add stampede protection via probabilistic early expiration (nicolas-grekas)
This PR was merged into the 4.2-dev branch.
Discussion
----------
[Cache] Add stampede protection via probabilistic early expiration
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | yes
| Tests pass? | yes
| Fixed tickets | -
| License | MIT
| Doc PR |
This PR implements [probabilistic early expiration](https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration) on top of `$cache->get($key, $callback);`
It adds a 3rd arg to `CacheInterface::get`:
> float $beta A float that controls the likelyness of triggering early expiration. 0 disables it, INF forces immediate expiration. The default is implementation dependend but should typically be 1.0, which should provide optimal stampede protection.
Commits
-------
13523ad985
[Cache] Add stampede protection via probabilistic early expiration
This commit is contained in:
commit
7e3b7b0b50
@ -1,6 +1,11 @@
|
||||
UPGRADE FROM 4.1 to 4.2
|
||||
=======================
|
||||
|
||||
Cache
|
||||
-----
|
||||
|
||||
* Deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead.
|
||||
|
||||
Security
|
||||
--------
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
UPGRADE FROM 4.x to 5.0
|
||||
=======================
|
||||
|
||||
Cache
|
||||
-----
|
||||
|
||||
* Removed `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead.
|
||||
|
||||
Config
|
||||
------
|
||||
|
||||
|
@ -46,9 +46,18 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
|
||||
function ($key, $value, $isHit) use ($defaultLifetime) {
|
||||
$item = new CacheItem();
|
||||
$item->key = $key;
|
||||
$item->value = $value;
|
||||
$item->value = $v = $value;
|
||||
$item->isHit = $isHit;
|
||||
$item->defaultLifetime = $defaultLifetime;
|
||||
// Detect wrapped values that encode for their expiry and creation duration
|
||||
// For compactness, these values are packed in the key of an array using
|
||||
// magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
|
||||
if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = \key($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) {
|
||||
$item->value = $v[$k];
|
||||
$v = \unpack('Ve/Nc', \substr($k, 1, -1));
|
||||
$item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
|
||||
$item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
|
||||
}
|
||||
|
||||
return $item;
|
||||
},
|
||||
@ -64,12 +73,18 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
|
||||
|
||||
foreach ($deferred as $key => $item) {
|
||||
if (null === $item->expiry) {
|
||||
$byLifetime[0 < $item->defaultLifetime ? $item->defaultLifetime : 0][$getId($key)] = $item->value;
|
||||
$ttl = 0 < $item->defaultLifetime ? $item->defaultLifetime : 0;
|
||||
} elseif ($item->expiry > $now) {
|
||||
$byLifetime[$item->expiry - $now][$getId($key)] = $item->value;
|
||||
$ttl = $item->expiry - $now;
|
||||
} else {
|
||||
$expiredIds[] = $getId($key);
|
||||
continue;
|
||||
}
|
||||
if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
|
||||
unset($metadata[CacheItem::METADATA_TAGS]);
|
||||
}
|
||||
// For compactness, expiry and creation duration are packed in the key of a array, using magic numbers as separators
|
||||
$byLifetime[$ttl][$getId($key)] = $metadata ? array("\x9D".pack('VN', (int) $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET, $metadata[CacheItem::METADATA_CTIME])."\x5F" => $item->value) : $item->value;
|
||||
}
|
||||
|
||||
return $byLifetime;
|
||||
|
@ -64,8 +64,10 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
|
||||
$item->value = $sourceItem->value;
|
||||
$item->expiry = $sourceItem->expiry;
|
||||
$item->isHit = $sourceItem->isHit;
|
||||
$item->metadata = $sourceItem->metadata;
|
||||
|
||||
$sourceItem->isTaggable = false;
|
||||
unset($sourceItem->metadata[CacheItem::METADATA_TAGS]);
|
||||
|
||||
if (0 < $sourceItem->defaultLifetime && $sourceItem->defaultLifetime < $defaultLifetime) {
|
||||
$defaultLifetime = $sourceItem->defaultLifetime;
|
||||
@ -84,19 +86,20 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get(string $key, callable $callback)
|
||||
public function get(string $key, callable $callback, float $beta = null)
|
||||
{
|
||||
$lastItem = null;
|
||||
$i = 0;
|
||||
$wrap = function (CacheItem $item = null) use ($key, $callback, &$wrap, &$i, &$lastItem) {
|
||||
$wrap = function (CacheItem $item = null) use ($key, $callback, $beta, &$wrap, &$i, &$lastItem) {
|
||||
$adapter = $this->adapters[$i];
|
||||
if (isset($this->adapters[++$i])) {
|
||||
$callback = $wrap;
|
||||
$beta = INF === $beta ? INF : 0;
|
||||
}
|
||||
if ($adapter instanceof CacheInterface) {
|
||||
$value = $adapter->get($key, $callback);
|
||||
$value = $adapter->get($key, $callback, $beta);
|
||||
} else {
|
||||
$value = $this->doGet($adapter, $key, $callback);
|
||||
$value = $this->doGet($adapter, $key, $callback, $beta ?? 1.0);
|
||||
}
|
||||
if (null !== $item) {
|
||||
($this->syncItem)($lastItem = $lastItem ?? $item, $item);
|
||||
|
@ -83,17 +83,17 @@ class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInte
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get(string $key, callable $callback)
|
||||
public function get(string $key, callable $callback, float $beta = null)
|
||||
{
|
||||
if (null === $this->values) {
|
||||
$this->initialize();
|
||||
}
|
||||
if (null === $value = $this->values[$key] ?? null) {
|
||||
if ($this->pool instanceof CacheInterface) {
|
||||
return $this->pool->get($key, $callback);
|
||||
return $this->pool->get($key, $callback, $beta);
|
||||
}
|
||||
|
||||
return $this->doGet($this->pool, $key, $callback);
|
||||
return $this->doGet($this->pool, $key, $callback, $beta ?? 1.0);
|
||||
}
|
||||
if ('N;' === $value) {
|
||||
return null;
|
||||
|
@ -31,6 +31,7 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
|
||||
private $namespace;
|
||||
private $namespaceLen;
|
||||
private $createCacheItem;
|
||||
private $setInnerItem;
|
||||
private $poolHash;
|
||||
|
||||
public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0)
|
||||
@ -43,11 +44,22 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
|
||||
function ($key, $innerItem) use ($defaultLifetime, $poolHash) {
|
||||
$item = new CacheItem();
|
||||
$item->key = $key;
|
||||
$item->value = $innerItem->get();
|
||||
$item->value = $v = $innerItem->get();
|
||||
$item->isHit = $innerItem->isHit();
|
||||
$item->defaultLifetime = $defaultLifetime;
|
||||
$item->innerItem = $innerItem;
|
||||
$item->poolHash = $poolHash;
|
||||
// Detect wrapped values that encode for their expiry and creation duration
|
||||
// For compactness, these values are packed in the key of an array using
|
||||
// magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
|
||||
if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = \key($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) {
|
||||
$item->value = $v[$k];
|
||||
$v = \unpack('Ve/Nc', \substr($k, 1, -1));
|
||||
$item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
|
||||
$item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
|
||||
} elseif ($innerItem instanceof CacheItem) {
|
||||
$item->metadata = $innerItem->metadata;
|
||||
}
|
||||
$innerItem->set(null);
|
||||
|
||||
return $item;
|
||||
@ -55,20 +67,43 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
|
||||
null,
|
||||
CacheItem::class
|
||||
);
|
||||
$this->setInnerItem = \Closure::bind(
|
||||
/**
|
||||
* @param array $item A CacheItem cast to (array); accessing protected properties requires adding the \0*\0" PHP prefix
|
||||
*/
|
||||
function (CacheItemInterface $innerItem, array $item) {
|
||||
// Tags are stored separately, no need to account for them when considering this item's newly set metadata
|
||||
if (isset(($metadata = $item["\0*\0newMetadata"])[CacheItem::METADATA_TAGS])) {
|
||||
unset($metadata[CacheItem::METADATA_TAGS]);
|
||||
}
|
||||
if ($metadata) {
|
||||
// For compactness, expiry and creation duration are packed in the key of a array, using magic numbers as separators
|
||||
$item["\0*\0value"] = array("\x9D".pack('VN', (int) $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET, $metadata[CacheItem::METADATA_CTIME])."\x5F" => $item["\0*\0value"]);
|
||||
}
|
||||
$innerItem->set($item["\0*\0value"]);
|
||||
$innerItem->expiresAt(null !== $item["\0*\0expiry"] ? \DateTime::createFromFormat('U', $item["\0*\0expiry"]) : null);
|
||||
},
|
||||
null,
|
||||
CacheItem::class
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get(string $key, callable $callback)
|
||||
public function get(string $key, callable $callback, float $beta = null)
|
||||
{
|
||||
if (!$this->pool instanceof CacheInterface) {
|
||||
return $this->doGet($this->pool, $key, $callback);
|
||||
return $this->doGet($this, $key, $callback, $beta ?? 1.0);
|
||||
}
|
||||
|
||||
return $this->pool->get($this->getId($key), function ($innerItem) use ($key, $callback) {
|
||||
return $callback(($this->createCacheItem)($key, $innerItem));
|
||||
});
|
||||
$item = ($this->createCacheItem)($key, $innerItem);
|
||||
$item->set($value = $callback($item));
|
||||
($this->setInnerItem)($innerItem, (array) $item);
|
||||
|
||||
return $value;
|
||||
}, $beta);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -164,13 +199,11 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
|
||||
return false;
|
||||
}
|
||||
$item = (array) $item;
|
||||
$expiry = $item["\0*\0expiry"];
|
||||
if (null === $expiry && 0 < $item["\0*\0defaultLifetime"]) {
|
||||
$expiry = time() + $item["\0*\0defaultLifetime"];
|
||||
if (null === $item["\0*\0expiry"] && 0 < $item["\0*\0defaultLifetime"]) {
|
||||
$item["\0*\0expiry"] = time() + $item["\0*\0defaultLifetime"];
|
||||
}
|
||||
$innerItem = $item["\0*\0poolHash"] === $this->poolHash ? $item["\0*\0innerItem"] : $this->pool->getItem($this->namespace.$item["\0*\0key"]);
|
||||
$innerItem->set($item["\0*\0value"]);
|
||||
$innerItem->expiresAt(null !== $expiry ? \DateTime::createFromFormat('U', $expiry) : null);
|
||||
($this->setInnerItem)($innerItem, $item);
|
||||
|
||||
return $this->pool->$method($innerItem);
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ class TagAwareAdapter implements CacheInterface, TagAwareAdapterInterface, Prune
|
||||
}
|
||||
if (isset($itemTags[$key])) {
|
||||
foreach ($itemTags[$key] as $tag => $version) {
|
||||
$item->prevTags[$tag] = $tag;
|
||||
$item->metadata[CacheItem::METADATA_TAGS][$tag] = $tag;
|
||||
}
|
||||
unset($itemTags[$key]);
|
||||
} else {
|
||||
@ -84,7 +84,7 @@ class TagAwareAdapter implements CacheInterface, TagAwareAdapterInterface, Prune
|
||||
function ($deferred) {
|
||||
$tagsByKey = array();
|
||||
foreach ($deferred as $key => $item) {
|
||||
$tagsByKey[$key] = $item->tags;
|
||||
$tagsByKey[$key] = $item->newMetadata[CacheItem::METADATA_TAGS] ?? array();
|
||||
}
|
||||
|
||||
return $tagsByKey;
|
||||
|
@ -37,7 +37,7 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get(string $key, callable $callback)
|
||||
public function get(string $key, callable $callback, float $beta = null)
|
||||
{
|
||||
if (!$this->pool instanceof CacheInterface) {
|
||||
throw new \BadMethodCallException(sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_class($this->pool), CacheInterface::class));
|
||||
@ -52,7 +52,7 @@ class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInt
|
||||
|
||||
$event = $this->start(__FUNCTION__);
|
||||
try {
|
||||
$value = $this->pool->get($key, $callback);
|
||||
$value = $this->pool->get($key, $callback, $beta);
|
||||
$event->result[$key] = \is_object($value) ? \get_class($value) : gettype($value);
|
||||
} finally {
|
||||
$event->end = microtime(true);
|
||||
|
@ -4,8 +4,9 @@ CHANGELOG
|
||||
4.2.0
|
||||
-----
|
||||
|
||||
* added `CacheInterface`, which should become the preferred way to use a cache
|
||||
* added `CacheInterface`, which provides stampede protection via probabilistic early expiration and should become the preferred way to use a cache
|
||||
* throw `LogicException` when `CacheItem::tag()` is called on an item coming from a non tag-aware pool
|
||||
* deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead
|
||||
|
||||
3.4.0
|
||||
-----
|
||||
@ -19,7 +20,7 @@ CHANGELOG
|
||||
3.3.0
|
||||
-----
|
||||
|
||||
* [EXPERIMENTAL] added CacheItem::getPreviousTags() to get bound tags coming from the pool storage if any
|
||||
* added CacheItem::getPreviousTags() to get bound tags coming from the pool storage if any
|
||||
* added PSR-16 "Simple Cache" implementations for all existing PSR-6 adapters
|
||||
* added Psr6Cache and SimpleCacheAdapter for bidirectional interoperability between PSR-6 and PSR-16
|
||||
* added MemcachedAdapter (PSR-6) and MemcachedCache (PSR-16)
|
||||
|
@ -26,8 +26,12 @@ interface CacheInterface
|
||||
{
|
||||
/**
|
||||
* @param callable(CacheItem):mixed $callback Should return the computed value for the given key/item
|
||||
* @param float|null $beta A float that controls the likeliness of triggering early expiration.
|
||||
* 0 disables it, INF forces immediate expiration.
|
||||
* The default (or providing null) is implementation dependent but should
|
||||
* typically be 1.0, which should provide optimal stampede protection.
|
||||
*
|
||||
* @return mixed The value corresponding to the provided key
|
||||
*/
|
||||
public function get(string $key, callable $callback);
|
||||
public function get(string $key, callable $callback, float $beta = null);
|
||||
}
|
||||
|
@ -21,13 +21,30 @@ use Symfony\Component\Cache\Exception\LogicException;
|
||||
*/
|
||||
final class CacheItem implements CacheItemInterface
|
||||
{
|
||||
/**
|
||||
* References the Unix timestamp stating when the item will expire.
|
||||
*/
|
||||
const METADATA_EXPIRY = 'expiry';
|
||||
|
||||
/**
|
||||
* References the time the item took to be created, in milliseconds.
|
||||
*/
|
||||
const METADATA_CTIME = 'ctime';
|
||||
|
||||
/**
|
||||
* References the list of tags that were assigned to the item, as string[].
|
||||
*/
|
||||
const METADATA_TAGS = 'tags';
|
||||
|
||||
private const METADATA_EXPIRY_OFFSET = 1527506807;
|
||||
|
||||
protected $key;
|
||||
protected $value;
|
||||
protected $isHit = false;
|
||||
protected $expiry;
|
||||
protected $defaultLifetime;
|
||||
protected $tags = array();
|
||||
protected $prevTags = array();
|
||||
protected $metadata = array();
|
||||
protected $newMetadata = array();
|
||||
protected $innerItem;
|
||||
protected $poolHash;
|
||||
protected $isTaggable = false;
|
||||
@ -121,7 +138,7 @@ final class CacheItem implements CacheItemInterface
|
||||
if (!\is_string($tag)) {
|
||||
throw new InvalidArgumentException(sprintf('Cache tag must be string, "%s" given', is_object($tag) ? get_class($tag) : gettype($tag)));
|
||||
}
|
||||
if (isset($this->tags[$tag])) {
|
||||
if (isset($this->newMetadata[self::METADATA_TAGS][$tag])) {
|
||||
continue;
|
||||
}
|
||||
if ('' === $tag) {
|
||||
@ -130,7 +147,7 @@ final class CacheItem implements CacheItemInterface
|
||||
if (false !== strpbrk($tag, '{}()/\@:')) {
|
||||
throw new InvalidArgumentException(sprintf('Cache tag "%s" contains reserved characters {}()/\@:', $tag));
|
||||
}
|
||||
$this->tags[$tag] = $tag;
|
||||
$this->newMetadata[self::METADATA_TAGS][$tag] = $tag;
|
||||
}
|
||||
|
||||
return $this;
|
||||
@ -140,10 +157,24 @@ final class CacheItem implements CacheItemInterface
|
||||
* Returns the list of tags bound to the value coming from the pool storage if any.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @deprecated since Symfony 4.2, use the "getMetadata()" method instead.
|
||||
*/
|
||||
public function getPreviousTags()
|
||||
{
|
||||
return $this->prevTags;
|
||||
@trigger_error(sprintf('The "%s" method is deprecated since Symfony 4.2, use the "getMetadata()" method instead.', __METHOD__), E_USER_DEPRECATED);
|
||||
|
||||
return $this->metadata[self::METADATA_TAGS] ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of metadata info that were saved alongside with the cached value.
|
||||
*
|
||||
* See public CacheItem::METADATA_* consts for keys potentially found in the returned array.
|
||||
*/
|
||||
public function getMetadata(): array
|
||||
{
|
||||
return $this->metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,6 +45,42 @@ abstract class AdapterTestCase extends CachePoolTest
|
||||
|
||||
$item = $cache->getItem('foo');
|
||||
$this->assertSame($value, $item->get());
|
||||
|
||||
$isHit = true;
|
||||
$this->assertSame($value, $cache->get('foo', function (CacheItem $item) use (&$isHit) { $isHit = false; }, 0));
|
||||
$this->assertTrue($isHit);
|
||||
|
||||
$this->assertNull($cache->get('foo', function (CacheItem $item) use (&$isHit, $value) {
|
||||
$isHit = false;
|
||||
$this->assertTrue($item->isHit());
|
||||
$this->assertSame($value, $item->get());
|
||||
}, INF));
|
||||
$this->assertFalse($isHit);
|
||||
}
|
||||
|
||||
public function testGetMetadata()
|
||||
{
|
||||
if (isset($this->skippedTests[__FUNCTION__])) {
|
||||
$this->markTestSkipped($this->skippedTests[__FUNCTION__]);
|
||||
}
|
||||
|
||||
$cache = $this->createCachePool(0, __FUNCTION__);
|
||||
|
||||
$cache->deleteItem('foo');
|
||||
$cache->get('foo', function ($item) {
|
||||
$item->expiresAfter(10);
|
||||
sleep(1);
|
||||
|
||||
return 'bar';
|
||||
});
|
||||
|
||||
$item = $cache->getItem('foo');
|
||||
|
||||
$expected = array(
|
||||
CacheItem::METADATA_EXPIRY => 9 + time(),
|
||||
CacheItem::METADATA_CTIME => 1000,
|
||||
);
|
||||
$this->assertSame($expected, $item->getMetadata());
|
||||
}
|
||||
|
||||
public function testDefaultLifeTime()
|
||||
|
@ -19,6 +19,7 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
class ArrayAdapterTest extends AdapterTestCase
|
||||
{
|
||||
protected $skippedTests = array(
|
||||
'testGetMetadata' => 'ArrayAdapter does not keep metadata.',
|
||||
'testDeferredSaveWithoutCommit' => 'Assumes a shared cache which ArrayAdapter is not.',
|
||||
'testSaveWithoutExpire' => 'Assumes a shared cache which ArrayAdapter is not.',
|
||||
);
|
||||
|
@ -24,8 +24,12 @@ use Symfony\Component\Cache\Tests\Fixtures\ExternalAdapter;
|
||||
*/
|
||||
class ChainAdapterTest extends AdapterTestCase
|
||||
{
|
||||
public function createCachePool($defaultLifetime = 0)
|
||||
public function createCachePool($defaultLifetime = 0, $testMethod = null)
|
||||
{
|
||||
if ('testGetMetadata' === $testMethod) {
|
||||
return new ChainAdapter(array(new FilesystemAdapter('', $defaultLifetime)), $defaultLifetime);
|
||||
}
|
||||
|
||||
return new ChainAdapter(array(new ArrayAdapter($defaultLifetime), new ExternalAdapter(), new FilesystemAdapter('', $defaultLifetime)), $defaultLifetime);
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Component\Cache\Tests\Adapter;
|
||||
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
use Symfony\Component\Cache\Adapter\ProxyAdapter;
|
||||
|
||||
/**
|
||||
@ -19,8 +20,12 @@ use Symfony\Component\Cache\Adapter\ProxyAdapter;
|
||||
*/
|
||||
class NamespacedProxyAdapterTest extends ProxyAdapterTest
|
||||
{
|
||||
public function createCachePool($defaultLifetime = 0)
|
||||
public function createCachePool($defaultLifetime = 0, $testMethod = null)
|
||||
{
|
||||
if ('testGetMetadata' === $testMethod) {
|
||||
return new ProxyAdapter(new FilesystemAdapter(), 'foo', $defaultLifetime);
|
||||
}
|
||||
|
||||
return new ProxyAdapter(new ArrayAdapter($defaultLifetime), 'foo', $defaultLifetime);
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Component\Cache\Tests\Adapter;
|
||||
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
use Symfony\Component\Cache\Adapter\NullAdapter;
|
||||
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
|
||||
|
||||
@ -68,8 +69,12 @@ class PhpArrayAdapterTest extends AdapterTestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function createCachePool()
|
||||
public function createCachePool($defaultLifetime = 0, $testMethod = null)
|
||||
{
|
||||
if ('testGetMetadata' === $testMethod) {
|
||||
return new PhpArrayAdapter(self::$file, new FilesystemAdapter());
|
||||
}
|
||||
|
||||
return new PhpArrayAdapterWrapper(self::$file, new NullAdapter());
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ namespace Symfony\Component\Cache\Tests\Adapter;
|
||||
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
use Symfony\Component\Cache\Adapter\ProxyAdapter;
|
||||
use Symfony\Component\Cache\CacheItem;
|
||||
|
||||
@ -27,8 +28,12 @@ class ProxyAdapterTest extends AdapterTestCase
|
||||
'testPrune' => 'ProxyAdapter just proxies',
|
||||
);
|
||||
|
||||
public function createCachePool($defaultLifetime = 0)
|
||||
public function createCachePool($defaultLifetime = 0, $testMethod = null)
|
||||
{
|
||||
if ('testGetMetadata' === $testMethod) {
|
||||
return new ProxyAdapter(new FilesystemAdapter(), '', $defaultLifetime);
|
||||
}
|
||||
|
||||
return new ProxyAdapter(new ArrayAdapter(), '', $defaultLifetime);
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ namespace Symfony\Component\Cache\Tests\Adapter;
|
||||
use Symfony\Component\Cache\Adapter\AdapterInterface;
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
|
||||
use Symfony\Component\Cache\CacheItem;
|
||||
|
||||
/**
|
||||
* @group time-sensitive
|
||||
@ -138,6 +139,9 @@ class TagAwareAdapterTest extends AdapterTestCase
|
||||
$this->assertFalse($pool->getItem('foo')->isHit());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
*/
|
||||
public function testGetPreviousTags()
|
||||
{
|
||||
$pool = $this->createCachePool();
|
||||
@ -149,6 +153,17 @@ class TagAwareAdapterTest extends AdapterTestCase
|
||||
$this->assertSame(array('foo' => 'foo'), $i->getPreviousTags());
|
||||
}
|
||||
|
||||
public function testGetMetadata()
|
||||
{
|
||||
$pool = $this->createCachePool();
|
||||
|
||||
$i = $pool->getItem('k');
|
||||
$pool->save($i->tag('foo'));
|
||||
|
||||
$i = $pool->getItem('k');
|
||||
$this->assertSame(array('foo' => 'foo'), $i->getMetadata()[CacheItem::METADATA_TAGS]);
|
||||
}
|
||||
|
||||
public function testPrune()
|
||||
{
|
||||
$cache = new TagAwareAdapter($this->getPruneableMock());
|
||||
|
@ -63,7 +63,7 @@ class CacheItemTest extends TestCase
|
||||
$this->assertSame($item, $item->tag(array('bar', 'baz')));
|
||||
|
||||
call_user_func(\Closure::bind(function () use ($item) {
|
||||
$this->assertSame(array('foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz'), $item->tags);
|
||||
$this->assertSame(array('foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz'), $item->newMetadata[CacheItem::METADATA_TAGS]);
|
||||
}, $this, CacheItem::class));
|
||||
}
|
||||
|
||||
|
@ -11,9 +11,15 @@
|
||||
|
||||
namespace Symfony\Component\Cache\Traits;
|
||||
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Component\Cache\CacheItem;
|
||||
|
||||
/**
|
||||
* An implementation for CacheInterface that provides stampede protection via probabilistic early expiration.
|
||||
*
|
||||
* @see https://en.wikipedia.org/wiki/Cache_stampede
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
@ -23,21 +29,58 @@ trait GetTrait
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get(string $key, callable $callback)
|
||||
public function get(string $key, callable $callback, float $beta = null)
|
||||
{
|
||||
return $this->doGet($this, $key, $callback);
|
||||
return $this->doGet($this, $key, $callback, $beta ?? 1.0);
|
||||
}
|
||||
|
||||
private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback)
|
||||
private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback, float $beta)
|
||||
{
|
||||
$t = 0;
|
||||
$item = $pool->getItem($key);
|
||||
$recompute = !$item->isHit() || INF === $beta;
|
||||
|
||||
if ($item->isHit()) {
|
||||
if ($item instanceof CacheItem && 0 < $beta) {
|
||||
if ($recompute) {
|
||||
$t = microtime(true);
|
||||
} else {
|
||||
$metadata = $item->getMetadata();
|
||||
$expiry = $metadata[CacheItem::METADATA_EXPIRY] ?? false;
|
||||
$ctime = $metadata[CacheItem::METADATA_CTIME] ?? false;
|
||||
|
||||
if ($ctime && $expiry) {
|
||||
$t = microtime(true);
|
||||
$recompute = $expiry <= $t - $ctime / 1000 * $beta * log(random_int(1, PHP_INT_MAX) / PHP_INT_MAX);
|
||||
}
|
||||
}
|
||||
if ($recompute) {
|
||||
// force applying defaultLifetime to expiry
|
||||
$item->expiresAt(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$recompute) {
|
||||
return $item->get();
|
||||
}
|
||||
|
||||
$pool->save($item->set($value = $callback($item)));
|
||||
static $save = null;
|
||||
|
||||
return $value;
|
||||
if (null === $save) {
|
||||
$save = \Closure::bind(
|
||||
function (CacheItemPoolInterface $pool, CacheItemInterface $item, $value, float $startTime) {
|
||||
if ($item instanceof CacheItem && $startTime && $item->expiry > $endTime = microtime(true)) {
|
||||
$item->newMetadata[CacheItem::METADATA_EXPIRY] = $item->expiry;
|
||||
$item->newMetadata[CacheItem::METADATA_CTIME] = 1000 * (int) ($endTime - $startTime);
|
||||
}
|
||||
$pool->save($item->set($value));
|
||||
|
||||
return $value;
|
||||
},
|
||||
null,
|
||||
CacheItem::class
|
||||
);
|
||||
}
|
||||
|
||||
return $save($pool, $item, $callback($item), $t);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user