8000 [Cache] Prevent stampede at warmup using apcu_entry() for locking · symfony/symfony@e31510c · GitHub
[go: up one dir, main page]

Skip to content
10000

Commit e31510c

Browse files
[Cache] Prevent stampede at warmup using apcu_entry() for locking
1 parent 5c338cc commit e31510c

File tree

5 files changed

+60
-3
lines changed

5 files changed

+60
-3
lines changed

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

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

160+
/**
161+
* {@inheritdoc}
162+
*/
163+
public function get(string $key, callable $callback, float $beta = null)
164+
{
165+
return $this->doGet($this, $key, $callback, $beta ?? 1.0, '' !== $this->namespace ? $this->getId($key) : null);
166+
}
167+
160168
/**
161169
* {@inheritdoc}
162170
*/

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ function (CacheItemInterface $innerItem, array $item) {
9494
public function get(string $key, callable $callback, float $beta = null)
9595
{
9696
if (!$this->pool instanceof CacheInterface) {
97-
return $this->doGet($this, $key, $callback, $beta ?? 1.0);
97+
return $this->doGet($this, $key, $callback, $beta ?? 1.0, $this->namespaceLen ? $this->getId($key) : null);
9898
}
9999

100100
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 CacheInterface, TagAwareAdapterInterface, Prune
3535
private $setCacheItemTags;
3636
private $getTagsByKey;
3737
private $invalidateTags;
38+
private $getId;
3839
private $tags;
3940
private $knownTagVersions = array();
4041
private $knownTagVersionsTtl;
@@ -105,6 +106,13 @@ function (AdapterInterface $tagsAdapter, array $tags) {
105106
null,
106107
CacheItem::class
107108
);
109+
$this->getId = \Closure::bind(
110+
function (AbstractAdapter $pool, $key) {
111+
return $pool->getId($key);
112+
},
113+
null,
114+
AbstractAdapter::class
115+
);
108116
}
109117

110118
/**
@@ -150,6 +158,22 @@ public function invalidateTags(array $tags)
150158
return $ok;
151159
}
152160

161+
/**
162+
* {@inheritdoc}
163+
*/
164+
public function get(string $key, callable $callback, float $beta = null)
165+
{
166+
if ($this->pool instanceof AbstractAdapter) {
167+
$id = ($this->getId)($this->pool, $key);
168+
} elseif ($this->pool instanceof ProxyAdapter) {
169+
$id = ((array) $this->pool)["\0Symfony\\Component\\Cache\\Adapter\\ProxyAdapter\0namespace"].$key;
170+
} else {
171+
$id = null;
172+
}
173+
174+
return $this->doGet($this, $key, $callback, $beta ?? 1.0, $id !== $key ? $id : null);
175+
}
176+
153177
/**
154178
* {@inheritdoc}
155179
*/

src/Symfony/Component/Cache/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
-----
66

77
* added `CacheInterface`, which provides stampede protection via probabilistic early expiration and should become the preferred way to use a cache
8+
* added warmup-time stampede protection using `apcu_entry()` for locking when available
89
* added sub-second expiry accuracy for backends that support it
910
* throw `LogicException` when `CacheItem::tag()` is called on an item coming from a non tag-aware pool
1011
* deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead

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 fr 6020 om 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)
0