Skip to content

Commit 42588d7

Browse files
committed
[Cache] Add optimized FileSystem & Redis TagAware Adapters
Reduces cache lookups by 50% by changing logic of how tag information is stored to avoid having to look it up on getItem(s) calls. For Filesystem symlinks are used, for Redis "Set" datatype is used.
1 parent 3895acd commit 42588d7

File tree

3 files changed

+562
-0
lines changed

3 files changed

+562
-0
lines changed
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Symfony\Component\Cache\Adapter\TagAware;
15+
16+
use Psr\Cache\CacheItemInterface;
17+
use Psr\Log\LoggerAwareInterface;
18+
use Symfony\Component\Cache\Adapter\AdapterInterface;
19+
use Symfony\Component\Cache\CacheItem;
20+
use Symfony\Component\Cache\Exception\InvalidArgumentException;
21+
use Symfony\Component\Cache\ResettableInterface;
22+
use Symfony\Component\Cache\Traits\AbstractTrait;
23+
use Symfony\Component\Cache\Traits\ContractsTrait;
24+
use Symfony\Contracts\Cache\CacheInterface;
25+
26+
/**
27+
* @author Nicolas Grekas <p@tchwork.com>
28+
*/
29+
abstract class AbstractTagAwareAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
30+
{
31+
use AbstractTrait { getId as protected; }
32+
use ContractsTrait;
33+
34+
protected const TAGS_PREFIX = "\0tags\0";
35+
36+
private $createCacheItem;
37+
private $mergeByLifetime;
38+
39+
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
40+
{
41+
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
42+
if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
43+
throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s")', $this->maxIdLength - 24, \strlen($namespace), $namespace));
44+
}
45+
$this->createCacheItem = \Closure::bind(
46+
function ($key, $value, $isHit) use ($defaultLifetime) {
47+
$item = new CacheItem();
48+
$item->key = $key;
49+
$item->isHit = $isHit;
50+
$item->defaultLifetime = $defaultLifetime;
51+
//<diff:AbstractAdapter> extract Value and Tags from the cache value
52+
$item->value = $v = $value['value'];
53+
$item->metadata[CacheItem::METADATA_TAGS] = $value['tags'] ?? [];
54+
// Detect wrapped values that encode for their expiry and creation duration
55+
// For compactness, these values are packed
56+
if (isset($value['meta'])) {
57+
$v = \unpack('Ve/Nc', $value['meta']);
58+
$item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
59+
$item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
60+
}
61+
//</diff:AbstractAdapter>
62+
63+
64+
return $item;
65+
},
66+
null,
67+
CacheItem::class
68+
);
69+
$getId = \Closure::fromCallable([$this, 'getId']);
70+
$this->mergeByLifetime = \Closure::bind(
71+
function ($deferred, $namespace, &$expiredIds) use ($getId) {
72+
$byLifetime = [];
73+
$now = microtime(true);
74+
$expiredIds = [];
75+
76+
foreach ($deferred as $key => $item) {
77+
$key = (string) $key;
78+
if (null === $item->expiry) {
79+
$ttl = 0 < $item->defaultLifetime ? $item->defaultLifetime : 0;
80+
} elseif (0 >= $ttl = (int) ($item->expiry - $now)) {
81+
$expiredIds[] = $getId($key);
82+
continue;
83+
}
84+
//<diff:AbstractAdapter> store Value and Tags on the cache value
85+
if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
86+
$value = ['value' => $item->value, 'tags' => $metadata[CacheItem::METADATA_TAGS]];
87+
unset($metadata[CacheItem::METADATA_TAGS]);
88+
} else {
89+
$value = ['value' => $item->value, 'tags' => []];
90+
}
91+
92+
if ($metadata) {
93+
// For compactness, expiry and creation duration are packed, using magic numbers as separators
94+
$value['meta'] = pack('VN', (int) $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET, $metadata[CacheItem::METADATA_CTIME]);
95+
}
96+
$byLifetime[$ttl][$getId($key)] = $value;
97+
//</diff:AbstractAdapter>
98+
}
99+
100+
return $byLifetime;
101+
},
102+
null,
103+
CacheItem::class
104+
);
105+
}
106+
107+
/**
108+
* {@inheritdoc}
109+
*/
110+
public function getItem($key)
111+
{
112+
if ($this->deferred) {
113+
$this->commit();
114+
}
115+
$id = $this->getId($key);
116+
117+
$f = $this->createCacheItem;
118+
$isHit = false;
119+
$value = null;
120+
121+
try {
122+
foreach ($this->doFetch([$id]) as $value) {
123+
$isHit = true;
124+
}
125+
} catch (\Exception $e) {
126+
CacheItem::log($this->logger, 'Failed to fetch key "{key}"', ['key' => $key, 'exception' => $e]);
127+
}
128+
129+
return $f($key, $value, $isHit);
130+
}
131+
132+
/**
133+
* {@inheritdoc}
134+
*/
135+
public function getItems(array $keys = [])
136+
{
137+
if ($this->deferred) {
138+
$this->commit();
139+
}
140+
$ids = [];
141+
142+
foreach ($keys as $key) {
143+
$ids[] = $this->getId($key);
144+
}
145+
try {
146+
$items = $this->doFetch($ids);
147+
} catch (\Exception $e) {
148+
CacheItem::log($this->logger, 'Failed to fetch requested items', ['keys' => $keys, 'exception' => $e]);
149+
$items = [];
150+
}
151+
$ids = array_combine($ids, $keys);
152+
153+
return $this->generateItems($items, $ids);
154+
}
155+
156+
/**
157+
* {@inheritdoc}
158+
*/
159+
public function save(CacheItemInterface $item)
160+
{
161+
if (!$item instanceof CacheItem) {
162+
return false;
163+
}
164+
$this->deferred[$item->getKey()] = $item;
165+
166+
return $this->commit();
167+
}
168+
169+
/**
170+
* {@inheritdoc}
171+
*/
172+
public function saveDeferred(CacheItemInterface $item)
173+
{
174+
if (!$item instanceof CacheItem) {
175+
return false;
176+
}
177+
$this->deferred[$item->getKey()] = $item;
178+
179+
return true;
180+
}
181+
182+
/**
183+
* {@inheritdoc}
184+
*/
185+
public function commit()
186+
{
187+
$ok = true;
188+
$byLifetime = $this->mergeByLifetime;
189+
$byLifetime = $byLifetime($this->deferred, $this->namespace, $expiredIds);
190+
$retry = $this->deferred = [];
191+
192+
if ($expiredIds) {
193+
$this->doDelete($expiredIds);
194+
}
195+
foreach ($byLifetime as $lifetime => $values) {
196+
try {
197+
$e = $this->doSave($values, $lifetime);
198+
} catch (\Exception $e) {
199+
}
200+
if (true === $e || [] === $e) {
201+
continue;
202+
}
203+
if (\is_array($e) || 1 === \count($values)) {
204+
foreach (\is_array($e) ? $e : array_keys($values) as $id) {
205+
$ok = false;
206+
$v = $values[$id];
207+
$type = \is_object($v) ? \get_class($v) : \gettype($v);
208+
CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', ['key' => substr($id, \strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null]);
209+
}
210+
} else {
211+
foreach ($values as $id => $v) {
212+
$retry[$lifetime][] = $id;
213+
}
214+
}
215+
}
216+
217+
// When bulk-save failed, retry each item individually
218+
foreach ($retry as $lifetime => $ids) {
219+
foreach ($ids as $id) {
220+
try {
221+
$v = $byLifetime[$lifetime][$id];
222+
$e = $this->doSave([$id => $v], $lifetime);
223+
} catch (\Exception $e) {
224+
}
225+
if (true === $e || [] === $e) {
226+
continue;
227+
}
228+
$ok = false;
229+
$type = \is_object($v) ? \get_class($v) : \gettype($v);
230+
CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', ['key' => substr($id, \strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null]);
231+
}
232+
}
233+
234+
return $ok;
235+
}
236+
237+
public function __destruct()
238+
{
239+
if ($this->deferred) {
240+
$this->commit();
241+
}
242+
}
243+
244+
private function generateItems($items, &$keys)
245+
{
246+
$f = $this->createCacheItem;
247+
248+
try {
249+
foreach ($items as $id => $value) {
250+
if (!isset($keys[$id])) {
251+
$id = key($keys);
252+
}
253+
$key = $keys[$id];
254+
unset($keys[$id]);
255+
yield $key => $f($key, $value, true);
256+
}
257+
} catch (\Exception $e) {
258+
CacheItem::log($this->logger, 'Failed to fetch requested items', ['keys' => array_values($keys), 'exception' => $e]);
259+
}
260+
261+
foreach ($keys as $key) {
262+
yield $key => $f($key, null, false);
263+
}
264+
}
265+
}

0 commit comments

Comments
 (0)