Skip to content

Commit ec2b4c0

Browse files
[Cache] Prevent stampede at warmup using apcu_entry() for locking
1 parent d01a58b commit ec2b4c0

File tree

5 files changed

+61
-5
lines changed

5 files changed

+61
-5
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ public static function createConnection($dsn, array $options = array())
145145
throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn));
146146
}
147147

148+
/**
149+
* {@inheritdoc}
150+
*/
151+
public function get(string $key, callable $callback, float $beta = null)
152+
{
153+
return $this->doGet($this, $key, $callback, $beta ?? 1.0, '' !== $this->namespace ? $this->getId($key) : null);
154+
}
155+
148156
/**
149157
* {@inheritdoc}
150158
*/

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ function (CacheItemInterface $innerItem, array $item) {
8080
public function get(string $key, callable $callback, float $beta = null)
8181
{
8282
if (!$this->pool instanceof CacheInterface) {
83-
return $this->doGet($this, $key, $callback, $beta ?? 1.0);
83+
return $this->doGet($this, $key, $callback, $beta ?? 1.0, $this->namespaceLen ? $this->getId($key) : null);
8484
}
8585

8686
return $this->pool->get($this->getId($key), function ($innerItem) use ($key, $callback) {

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class TagAwareAdapter implements TagAwareAdapterInterface, TaggableCacheInterfac
3535
private $setCacheItemTags;
3636
private $getTagsByKey;
3737
private $invalidateTags;
38+
private $getId;
3839
private $tags;
3940

4041
public function __construct(AdapterInterface $itemsPool, AdapterInterface $tagsPool = null)
@@ -103,6 +104,13 @@ function (AdapterInterface $tagsAdapter, array $tags) {
103104
null,
104105
CacheItem::class
105106
);
107+
$this->getId = \Closure::bind(
108+
function (AbstractAdapter $pool, $key) {
109+
return $pool->getId($key);
110+
},
111+
null,
112+
AbstractAdapter::class
113+
);
106114
}
107115

108116
/**
@@ -120,6 +128,22 @@ public function invalidateTags(array $tags)
120128
return $f($this->tags, $tags);
121129
}
122130

131+
/**
132+
* {@inheritdoc}
133+
*/
134+
public function get(string $key, callable $callback, float $beta = null)
135+
{
136+
if ($this->pool instanceof AbstractAdapter) {
137+
$id = ($this->getId)($this->pool, $key);
138+
} elseif ($this->pool instanceof ProxyAdapter) {
139+
$id = ((array) $this->pool)["\0Symfony\\Component\\Cache\\Adapter\\ProxyAdapter\0namespace"].$key;
140+
} else {
141+
$id = null;
142+
}
143+
144+
return $this->doGet($this, $key, $callback, $beta ?? 1.0, $id !== $key ? $id : null);
145+
}
146+
123147
/**
124148
* {@inheritdoc}
125149
*/

src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ public function testGetStats()
7878

7979
$expected = array(
8080
CacheItem::STATS_CTIME => 1.0,
81-
CacheItem::STATS_EXPIRY => 9 + time(),
81+
CacheItem::STATS_EXPIRY => 8.5 + time(),
8282
);
83-
$this->assertSame($expected, $item->getStats());
83+
$this->assertEquals($expected, $item->getStats(), 'Item stats should be stored alongside with its value.', 0.5);
8484
}
8585

8686
public function testDefaultLifeTime()

src/Symfony/Component/Cache/Traits/GetTrait.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function get(string $key, callable $callback, float $beta = null)
3434
return $this->doGet($this, $key, $callback, $beta ?? 1.0);
3535
}
3636

37-
private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback, float $beta)
37+
private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback, float $beta, string $lockId = null)
3838
{
3939
$t = 0;
4040
$item = $pool->getItem($key);
@@ -64,6 +64,7 @@ private function doGet(CacheItemPoolInterface $pool, string $key, callable $call
6464
}
6565

6666
static $save = null;
67+
static $useApcu = null;
6768

6869
if (null === $save) {
6970
$save = \Closure::bind(
@@ -80,7 +81,30 @@ function (CacheItemPoolInterface $pool, CacheItemInterface $item, $value, float
8081
CacheItem::class
8182
);
8283
}
84+
if (null === $useApcu) {
85+
$useApcu = \function_exists('apcu_entry') && ini_get('apc.enabled') && ('cli' !== \PHP_SAPI || ini_get('apc.enable_cli'));
86+
}
87+
88+
if (null === $lockId || !$useApcu) {
89+
return $save($pool, $item, $callback($item), $t);
90+
}
91+
92+
$t = $t ?: microtime(true);
93+
$lockId = ':'.$lockId;
94+
$isHit = true;
95+
$value = apcu_entry($lockId, function () use ($callback, $item, &$isHit) {
96+
$isHit = false;
97+
98+
return $callback($item);
99+
}, 2);
100+
101+
if (!$isHit) {
102+
$save($pool, $item, $value, $t);
103+
// give concurrent processes 15% of the computation time to retrieve the value from APCu, up to 300ms
104+
usleep(min(300000, 150000 * (microtime(true) - $t)));
105+
apcu_delete($lockId);
106+
}
83107

84-
return $save($pool, $item, $callback($item), $t);
108+
return $value;
85109
}
86110
}

0 commit comments

Comments
 (0)