mirror of
https://github.com/symfony/cache.git
synced 2026-03-23 23:22:07 +01:00
[Cache] Fix ChainAdapter ignoring item expiry when propagating to earlier adapters
This commit is contained in:
committed by
Nicolas Grekas
parent
77f5eca135
commit
b643496715
@@ -34,6 +34,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
|
||||
private array $values = [];
|
||||
private array $tags = [];
|
||||
private array $expiries = [];
|
||||
private array $explicitExpiries = [];
|
||||
private int $defaultLifetime;
|
||||
private float $maxLifetime;
|
||||
private int $maxItems;
|
||||
@@ -58,7 +59,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
|
||||
$this->maxLifetime = $maxLifetime;
|
||||
$this->maxItems = $maxItems;
|
||||
self::$createCacheItem ??= \Closure::bind(
|
||||
static function ($key, $value, $isHit, $tags) {
|
||||
static function ($key, $value, $isHit, $tags, $expiry = null) {
|
||||
$item = new CacheItem();
|
||||
$item->key = $key;
|
||||
$item->value = $value;
|
||||
@@ -66,6 +67,9 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
|
||||
if (null !== $tags) {
|
||||
$item->metadata[CacheItem::METADATA_TAGS] = $tags;
|
||||
}
|
||||
if (null !== $expiry) {
|
||||
$item->metadata[CacheItem::METADATA_EXPIRY] = $expiry;
|
||||
}
|
||||
|
||||
return $item;
|
||||
},
|
||||
@@ -126,7 +130,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
|
||||
$value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
|
||||
}
|
||||
|
||||
return (self::$createCacheItem)($key, $value, $isHit, $this->tags[$key] ?? null);
|
||||
return (self::$createCacheItem)($key, $value, $isHit, $this->tags[$key] ?? null, $this->explicitExpiries[$key] ?? null);
|
||||
}
|
||||
|
||||
public function getItems(array $keys = []): iterable
|
||||
@@ -139,7 +143,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
|
||||
public function deleteItem(mixed $key): bool
|
||||
{
|
||||
\assert('' !== CacheItem::validateKey($key));
|
||||
unset($this->values[$key], $this->tags[$key], $this->expiries[$key]);
|
||||
unset($this->values[$key], $this->tags[$key], $this->expiries[$key], $this->explicitExpiries[$key]);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -193,13 +197,19 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
|
||||
break;
|
||||
}
|
||||
|
||||
unset($this->values[$k], $this->tags[$k], $this->expiries[$k]);
|
||||
unset($this->values[$k], $this->tags[$k], $this->expiries[$k], $this->explicitExpiries[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->values[$key] = $value;
|
||||
$this->expiries[$key] = $expiry ?? \PHP_INT_MAX;
|
||||
|
||||
if (null !== $item["\0*\0expiry"] && \PHP_INT_MAX !== $this->expiries[$key]) {
|
||||
$this->explicitExpiries[$key] = $this->expiries[$key];
|
||||
} else {
|
||||
unset($this->explicitExpiries[$key]);
|
||||
}
|
||||
|
||||
if (null === $this->tags[$key] = $item["\0*\0newMetadata"][CacheItem::METADATA_TAGS] ?? null) {
|
||||
unset($this->tags[$key]);
|
||||
}
|
||||
@@ -224,7 +234,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
|
||||
|
||||
foreach ($this->values as $key => $value) {
|
||||
if (!isset($this->expiries[$key]) || $this->expiries[$key] <= $now || str_starts_with($key, $prefix)) {
|
||||
unset($this->values[$key], $this->tags[$key], $this->expiries[$key]);
|
||||
unset($this->values[$key], $this->tags[$key], $this->expiries[$key], $this->explicitExpiries[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +243,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
|
||||
}
|
||||
}
|
||||
|
||||
$this->values = $this->tags = $this->expiries = [];
|
||||
$this->values = $this->tags = $this->expiries = $this->explicitExpiries = [];
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -290,7 +300,7 @@ class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInter
|
||||
}
|
||||
unset($keys[$i]);
|
||||
|
||||
yield $key => $f($key, $value, $isHit, $this->tags[$key] ?? null);
|
||||
yield $key => $f($key, $value, $isHit, $this->tags[$key] ?? null, $this->explicitExpiries[$key] ?? null);
|
||||
}
|
||||
|
||||
foreach ($keys as $key) {
|
||||
|
||||
@@ -79,6 +79,7 @@ class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
|
||||
$item->expiresAt(\DateTimeImmutable::createFromFormat('U.u', \sprintf('%.6F', $item->metadata[CacheItem::METADATA_EXPIRY])));
|
||||
} elseif (0 < $defaultLifetime) {
|
||||
$item->expiresAfter($defaultLifetime);
|
||||
$item->newMetadata[CacheItem::METADATA_EXPIRY] = $item->expiry;
|
||||
}
|
||||
|
||||
return $item;
|
||||
|
||||
@@ -170,6 +170,10 @@ final class CacheItem implements ItemInterface
|
||||
}
|
||||
$valueWrapper = self::VALUE_WRAPPER;
|
||||
|
||||
if ($this->value instanceof $valueWrapper) {
|
||||
return new $valueWrapper($this->value->value, $m + ['expiry' => $this->expiry] + $this->value->metadata);
|
||||
}
|
||||
|
||||
return new $valueWrapper($this->value, $m + ['expiry' => $this->expiry]);
|
||||
}
|
||||
|
||||
|
||||
@@ -197,6 +197,72 @@ class ChainAdapterTest extends AdapterTestCase
|
||||
$this->assertFalse($item->isHit());
|
||||
}
|
||||
|
||||
public function testItemExpiryIsPreservedWhenPropagatedToPreviousAdapters()
|
||||
{
|
||||
if (isset($this->skippedTests[__FUNCTION__])) {
|
||||
$this->markTestSkipped($this->skippedTests[__FUNCTION__]);
|
||||
}
|
||||
|
||||
$adapter1 = new ArrayAdapter(100);
|
||||
$adapter2 = new ArrayAdapter(100);
|
||||
|
||||
$cache = new ChainAdapter([$adapter1, $adapter2], 100);
|
||||
|
||||
// Save with an explicit 2-second TTL
|
||||
$cache->save($cache->getItem('key')->expiresAfter(2)->set('value'));
|
||||
|
||||
// Simulate adapter1 miss
|
||||
$adapter1->clear();
|
||||
$this->assertFalse($adapter1->hasItem('key'));
|
||||
$this->assertTrue($adapter2->hasItem('key'));
|
||||
|
||||
// This should propagate the item from adapter2 to adapter1,
|
||||
// preserving the original 2-second TTL (not the 100-second defaultLifetime)
|
||||
$cache->getItem('key');
|
||||
$this->assertTrue($adapter1->hasItem('key'));
|
||||
|
||||
sleep(3);
|
||||
|
||||
$this->assertFalse($adapter2->getItem('key')->isHit(), 'Item should have expired in adapter2');
|
||||
$this->assertFalse($adapter1->getItem('key')->isHit(), 'Item should have expired in adapter1 with original TTL, not defaultLifetime');
|
||||
}
|
||||
|
||||
public function testItemExpiryIsPreservedWhenPropagatedToPreviousAdaptersUsingGetMethod()
|
||||
{
|
||||
if (isset($this->skippedTests[__FUNCTION__])) {
|
||||
$this->markTestSkipped($this->skippedTests[__FUNCTION__]);
|
||||
}
|
||||
|
||||
$adapter1 = new ArrayAdapter(100);
|
||||
$adapter2 = new ArrayAdapter(100);
|
||||
|
||||
$cache = new ChainAdapter([$adapter1, $adapter2], 100);
|
||||
|
||||
// Save with an explicit 2-second TTL via get() callback
|
||||
$cache->get('key', static function (ItemInterface $item) {
|
||||
$item->expiresAfter(2);
|
||||
|
||||
return 'value';
|
||||
});
|
||||
|
||||
// Simulate adapter1 miss
|
||||
$adapter1->clear();
|
||||
$this->assertFalse($adapter1->hasItem('key'));
|
||||
$this->assertTrue($adapter2->hasItem('key'));
|
||||
|
||||
// This should propagate the item from adapter2 to adapter1,
|
||||
// preserving the original 2-second TTL (not the 100-second defaultLifetime)
|
||||
$cache->get('key', function () {
|
||||
$this->fail('Callback should not be called when item exists in adapter2');
|
||||
});
|
||||
$this->assertTrue($adapter1->hasItem('key'));
|
||||
|
||||
sleep(3);
|
||||
|
||||
$this->assertFalse($adapter2->getItem('key')->isHit(), 'Item should have expired in adapter2');
|
||||
$this->assertFalse($adapter1->getItem('key')->isHit(), 'Item should have expired in adapter1 with original TTL, not defaultLifetime');
|
||||
}
|
||||
|
||||
public function testExpirationOnAllAdapters()
|
||||
{
|
||||
if (isset($this->skippedTests[__FUNCTION__])) {
|
||||
@@ -231,7 +297,7 @@ class ChainAdapterTest extends AdapterTestCase
|
||||
->willReturn(true);
|
||||
|
||||
$cache = new ChainAdapter([$adapter1, $adapter2], 6);
|
||||
$cache->get('test_key', function (ItemInterface $item) {
|
||||
$cache->get('test_key', static function (ItemInterface $item) {
|
||||
$item->expiresAfter(15);
|
||||
|
||||
return 'chain';
|
||||
|
||||
Reference in New Issue
Block a user