8000 [Cache] Add stampede protection via probabilistic early expiration · symfony/symfony@d01a58b · GitHub
[go: up one dir, main page]

Skip to content

Commit d01a58b

Browse files
[Cache] Add stampede protection via probabilistic early expiration
1 parent 0bf5a3a commit d01a58b

21 files changed

+237
-44
lines changed

UPGRADE-4.2.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
UPGRADE FROM 4.1 to 4.2
2+
=======================
3+
4+
Cache
5+
-----
6+
7+
* Deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getStats()` instead.

UPGRADE-5.0.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
UPGRADE FROM 4.x to 5.0
22
=======================
33

4+
Cache
5+
-----
6+
7+
* Removed `CacheItem::getPreviousTags()`, use `CacheItem::getStats()` instead.
8+
49
Config
510
------
611

src/Symfony/Component/Cache/Adapter/AbstractAdapter.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,12 @@ protected function __construct(string $namespace = '', int $defaultLifetime = 0)
4646
function ($key, $value, $isHit) use ($defaultLifetime) {
4747
$item = new CacheItem();
4848
$item->key = $key;
49-
$item->value = $value;
49+
$item->value = $v = $value;
5050
$item->isHit = $isHit;
5151
$item->defaultLifetime = $defaultLifetime;
52+
if (\is_array($v) && 1 === \count($v) && \is_array($v = $v[CacheItem::STATS_KEY] ?? null) && array(0, 1) === \array_keys($v)) {
53+
list($item->value, $item->stats) = $v;
54+
}
5255

5356
return $item;
5457
},
@@ -64,12 +67,14 @@ function ($deferred, $namespace, &$expiredIds) use ($getId) {
6467

6568
foreach ($deferred as $key => $item) {
6669
if (null === $item->expiry) {
67-
$byLifetime[0 < $item->defaultLifetime ? $item->defaultLifetime : 0][$getId($key)] = $item->value;
70+
$ttl = 0 < $item->defaultLifetime ? $item->defaultLifetime : 0;
6871
} elseif ($item->expiry > $now) {
69-
$byLifetime[$item->expiry - $now][$getId($key)] = $item->value;
72+
$ttl = $item->expiry - $now;
7073
} else {
7174
$expiredIds[] = $getId($key);
75+
continue;
7276
}
77+
$byLifetime[$ttl][$getId($key)] = $item->newStats ? array(CacheItem::STATS_KEY => array($item->value, $item->newStats)) : $item->value;
7378
}
7479

7580
return $byLifetime;

src/Symfony/Component/Cache/Adapter/ChainAdapter.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,10 @@ function ($sourceItem, $item) use ($defaultLifetime) {
6464
$item->value = $sourceItem->value;
6565
$item->expiry = $sourceItem->expiry;
6666
$item->isHit = $sourceItem->isHit;
67+
$item->stats = $sourceItem->stats;
6768

6869
$sourceItem->isTaggable = false;
70+
unset($sourceItem->stats[CacheItem::STATS_TAGS]);
6971

7072
if (0 < $sourceItem->defaultLifetime && $sourceItem->defaultLifetime < $defaultLifetime) {
7173
$defaultLifetime = $sourceItem->defaultLifetime;
@@ -84,19 +86,20 @@ function ($sourceItem, $item) use ($defaultLifetime) {
8486
/**
8587
* {@inheritdoc}
8688
*/
87-
public function get(string $key, callable $callback)
89+
public function get(string $key, callable $callback, float $beta = null)
8890
{
8991
$lastItem = null;
9092
$i = 0;
91-
$wrap = function (CacheItem $item = null) use ($key, $callback, &$wrap, &$i, &$lastItem) {
93+
$wrap = function (CacheItem $item = null) use ($key, $callback, $beta, &$wrap, &$i, &$lastItem) {
9294
$adapter = $this->adapters[$i];
9395
if (isset($this->adapters[++$i])) {
9496
$callback = $wrap;
97+
$beta = INF === $beta ? INF : 0;
9598
}
9699
if ($adapter instanceof CacheInterface) {
97-
$value = $adapter->get($key, $callback);
100+
$value = $adapter->get($key, $callback, $beta);
98101
} else {
99-
$value = $this->doGet($adapter, $key, $callback);
102+
$value = $this->doGet($adapter, $key, $callback, $beta ?? 1.0);
100103
}
101104
if (null !== $item) {
102105
($this->syncItem)($lastItem = $lastItem ?? $item, $item);

src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,17 @@ public static function create($file, CacheItemPoolInterface $fallbackPool)
8383
/**
8484
* {@inheritdoc}
8585
*/
86-
public function get(string $key, callable $callback)
86+
public function get(string $key, callable $callback, float $beta = null)
8787
{
8888
if (null === $this->values) {
8989
$this->initialize();
9090
}
9191
if (null === $value = $this->values[$key] ?? null) {
9292
if ($this->pool instanceof CacheInterface) {
93-
return $this->pool->get($key, $callback);
93+
return $this->pool->get($key, $callback, $beta);
9494
}
9595

96-
return $this->doGet($this->pool, $key, $callback);
96+
return $this->doGet($this->pool, $key, $callback, $beta ?? 1.0);
9797
}
9898
if ('N;' === $value) {
9999
return null;

src/Symfony/Component/Cache/Adapter/ProxyAdapter.php

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterfa
3131
private $namespace;
3232
private $namespaceLen;
3333
private $createCacheItem;
34+
private $setInnerItem;
3435
private $poolHash;
3536

3637
public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0)
@@ -43,32 +44,52 @@ public function __construct(CacheItemPoolInterface $pool, string $namespace = ''
4344
function ($key, $innerItem) use ($defaultLifetime, $poolHash) {
4445
$item = new CacheItem();
4546
$item->key = $key;
46-
$item->value = $innerItem->get();
47+
$item->value = $v = $innerItem->get();
4748
$item->isHit = $innerItem->isHit();
4849
$item->defaultLifetime = $defaultLifetime;
4950
$item->innerItem = $innerItem;
5051
$item->poolHash = $poolHash;
52+
if (\is_array($v) && 1 === \count($v) && \is_array($v = $v[CacheItem::STATS_KEY] ?? null) && array(0, 1) === \array_keys($v)) {
53+
list($item->value, $item->stats) = $v;
54+
} elseif ($innerItem instanceof CacheItem) {
55+
$item->stats = $innerItem->stats;
56+
}
5157
$innerItem->set(null);
5258

5359
return $item;
5460
},
5561
null,
5662
CacheItem::class
5763
);
64+
$this->setInnerItem = \Closure::bind(
65+
function (CacheItemInterface $innerItem, array $item) {
66+
if ($stats = $item["\0*\0newStats"]) {
67+
$item["\0*\0value"] = array(CacheItem::STATS_KEY => array($item["\0*\0value"], $stats));
68+
}
69+
$innerItem->set($item["\0*\0value"]);
70+
$innerItem->expiresAt(null !== $item["\0*\0expiry"] ? \DateTime::createFromFormat('U', $item["\0*\0expiry"]) : null);
71+
},
72+
null,
73+
CacheItem::class
74+
);
5875
}
5976

6077
/**
6178
* {@inheritdoc}
6279
*/
63-
public function get(string $key, callable $callback)
80+
public function get(string $key, callable $callback, float $beta = null)
6481
{
6582
if (!$this->pool instanceof CacheInterface) {
66-
return $this->doGet($this->pool, $key, $callback);
83+
return $this->doGet($this, $key, $callback, $beta ?? 1.0);
6784
}
6885

6986
return $this->pool->get($this->getId($key), function ($innerItem) use ($key, $callback) {
70-
return $callback(($this->createCacheItem)($key, $innerItem));
71-
});
87+
$item = ($this->createCacheItem)($key, $innerItem);
88+
$item->set($value = $callback($item));
89+
($this->setInnerItem)($innerItem, (array) $item);
90+
91+
return $value;
92+
}, $beta);
7293
}
7394

7495
/**
@@ -164,13 +185,11 @@ private function doSave(CacheItemInterface $item, $method)
164185
return false;
165186
}
166187
$item = (array) $item;
167-
$expiry = $item["\0*\0expiry"];
168-
if (null === $expiry && 0 < $item["\0*\0defaultLifetime"]) {
169-
$expiry = time() + $item["\0*\0defaultLifetime"];
188+
if (null === $item["\0*\0expiry"] && 0 < $item["\0*\0defaultLifetime"]) {
189+
$item["\0*\0expiry"] = time() + $item["\0*\0defaultLifetime"];
170190
}
171191
$innerItem = $item["\0*\0poolHash"] === $this->poolHash ? $item["\0*\0innerItem"] : $this->pool->getItem($this->namespace.$item["\0*\0key"]);
172-
$innerItem->set($item["\0*\0value"]);
173-
$innerItem->expiresAt(null !== $expiry ? \DateTime::createFromFormat('U', $expiry) : null);
192+
($this->setInnerItem)($innerItem, $item);
174193

175194
return $this->pool->$method($innerItem);
176195
}

src/Symfony/Component/Cache/Adapter/TagAwareAdapter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ function (CacheItem $item, $key, array &$itemTags) {
6464
}
6565
if (isset($itemTags[$key])) {
6666
foreach ($itemTags[$key] as $tag => $version) {
67-
$item->prevTags[$tag] = $tag;
67+
$item->stats[CacheItem::STATS_TAGS][$tag] = $tag;
6868
}
6969
unset($itemTags[$key]);
7070
} else {
@@ -81,7 +81,7 @@ function (CacheItem $item, $key, array &$itemTags) {
8181
function ($deferred) {
8282
$tagsByKey = array();
8383
foreach ($deferred as $key => $item) {
84-
$tagsByKey[$key] = $item->tags;
84+
$tagsByKey[$key] = $item->newStats[CacheItem::STATS_TAGS] ?? array();
8585
}
8686

8787
return $tagsByKey;

src/Symfony/Component/Cache/Adapter/TraceableAdapter.php

Lines changed: 2 additions & 2 deletions
Original file line 10000 numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function __construct(AdapterInterface $pool)
3737
/**
3838
* {@inheritdoc}
3939
*/
40-
public function get(string $key, callable $callback)
40+
public function get(string $key, callable $callback, float $beta = null)
4141
{
4242
if (!$this->pool instanceof CacheInterface) {
4343
throw new \BadMethodCallException(sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_class($this->pool), CacheInterface::class));
@@ -52,7 +52,7 @@ public function get(string $key, callable $callback)
5252

5353
$event = $this->start(__FUNCTION__);
5454
try {
55-
$value = $this->pool->get($key, $callback);
55+
$value = $this->pool->get($key, $callback, $beta);
5656
$event->result[$key] = \is_object($value) ? \get_class($value) : gettype($value);
5757
} finally {
5858
$event->end = microtime(true);

src/Symfony/Component/Cache/CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ CHANGELOG
44
4.2.0
55
-----
66

7-
* added `CacheInterface` and `TaggableCacheInterface`
7+
* added `CacheInterface` and `TaggableCacheInterface`, providing stampede protection via probabilistic early expiration
88
* throw `LogicException` when `CacheItem::tag()` is called on an item coming from a non tag-aware pool
9+
* deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getStats()` instead
910

1011
3.4.0
1112
-----
@@ -19,7 +20,7 @@ CHANGELOG
1920
3.3.0
2021
-----
2122

22-
* [EXPERIMENTAL] added CacheItem::getPreviousTags() to get bound tags coming from the pool storage if any
23+
* added CacheItem::getPreviousTags() to get bound tags coming from the pool storage if any
2324
* added PSR-16 "Simple Cache" implementations for all existing PSR-6 adapters
2425
* added Psr6Cache and SimpleCacheAdapter for bidirectional interoperability between PSR-6 and PSR-16
2526
* added MemcachedAdapter (PSR-6) and MemcachedCache (PSR-16)

src/Symfony/Component/Cache/CacheInterface.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ interface CacheInterface
3030
{
3131
/**
3232
* @param callable(CacheItemInterface $item):mixed $callback Should return the computed value for the given key/item
33+
* @param float|null $beta A float that controls the likeliness of triggering early expiration.
34+
* 0 disables it, INF forces immediate expiration.
35+
* The default (or providing null) is implementation dependent but should
36+
* typically be 1.0, which should provide optimal stampede protection.
3337
*
3438
* @return mixed The value corresponding to the provided key
3539
*/
36-
public function get(string $key, callable $callback);
40+
public function get(string $key, callable $callback, float $beta = null);
3741
}

0 commit comments

Comments
 (0)
0