Skip to content

Commit eda0753

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 eda0753

File tree

3 files changed

+558
-0
lines changed

3 files changed

+558
-0
lines changed
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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+
return $item;
64+
},
65+
null,
66+
CacheItem::class
67+
);
68+
$getId = \Closure::fromCallable([$this, 'getId']);
69+
$this->mergeByLifetime = \Closure::bind(
70+
function ($deferred, $namespace, &$expiredIds) use ($getId) {
71+
$byLifetime = [];
72+
$now = microtime(true);
73+
$expiredIds = [];
74+
75+
foreach ($deferred as $key => $item) {
76+
$key = (string) $key;
77+
if (null === $item->expiry) {
78+
$ttl = 0 < $item->defaultLifetime ? $item->defaultLifetime : 0;
79+
} elseif (0 >= $ttl = (int) ($item->expiry - $now)) {
80+
$expiredIds[] = $getId($key);
81+
continue;
82+
}
83+
//<diff:AbstractAdapter> store Value and Tags on the cache value
84+
if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
85+
$value = ['value' => $item->value, 'tags' => $metadata[CacheItem::METADATA_TAGS]];
86+
unset($metadata[CacheItem::METADATA_TAGS]);
87+
} else {
88+
$value = ['value' => $item->value, 'tags' => []];
89+
}
90+
91+
if ($metadata) {
92+
// For compactness, expiry and creation duration are packed, using magic numbers as separators
93+
$value['meta'] = pack('VN', (int) $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET, $metadata[CacheItem::METADATA_CTIME]);
94+
}
95+
$byLifetime[$ttl][$getId($key)] = $value;
96+
//</diff:AbstractAdapter>
97+
}
98+
99+
return $byLifetime;
100+
},
101+
null,
102+
CacheItem::class
103+
);
104+
}
105+
106+
/**
107+
* {@inheritdoc}
108+
*/
109+
public function getItem($key)
110+
{
111+
if ($this->deferred) {
112+
$this->commit();
113+
}
114+
$id = $this->getId($key);
115+
116+
$f = $this->createCacheItem;
117+
$isHit = false;
118+
$value = null;
119+
120+
try {
121+
foreach ($this->doFetch([$id]) as $value) {
122+
$isHit = true;
123+
}
124+
} catch (\Exception $e) {
125+
CacheItem::log($this->logger, 'Failed to fetch key "{key}"', ['key' => $key, 'exception' => $e]);
126+
}
127+
128+
return $f($key, $value, $isHit);
129+
}
130+
131+
/**
132+
* {@inheritdoc}
133+
*/
134+
public function getItems(array $keys = [])
135+
{
136+
if ($this->deferred) {
137+
$this->commit();
138+
}
139+
$ids = [];
140+
141+
foreach ($keys as $key) {
142+
$ids[] = $this->getId($key);
143+
}
144+
try {
145+
$items = $this->doFetch($ids);
146+
} catch (\Exception $e) {
147+
CacheItem::log($this->logger, 'Failed to fetch requested items', ['keys' => $keys, 'exception' => $e]);
148+
$items = [];
149+
}
150+
$ids = array_combine($ids, $keys);
151+
152+
return $this->generateItems($items, $ids);
153+
}
154+
155+
/**
156+
* {@inheritdoc}
157+
*/
158+
public function save(CacheItemInterface $item)
159+
{
160+
if (!$item instanceof CacheItem) {
161+
return false;
162+
}
163+
$this->deferred[$item->getKey()] = $item;
164+
165+
return $this->commit();
166+
}
167+
168+
/**
169+
* {@inheritdoc}
170+
*/
171+
public function saveDeferred(CacheItemInterface $item)
172+
{
173+
if (!$item instanceof CacheItem) {
174+
return false;
175+
}
176+
$this->deferred[$item->getKey()] = $item;
177+
178+
return true;
179+
}
180+
181+
/**
182+
* {@inheritdoc}
183+
*/
184+
public function commit()
185+
{
186+
$ok = true;
187+
$byLifetime = $this->mergeByLifetime;
188+
$byLifetime = $byLifetime($this->deferred, $this->namespace, $expiredIds);
189+
$retry = $this->deferred = [];
190+
191+
if ($expiredIds) {
192+
$this->doDelete($expiredIds);
193+
}
194+
foreach ($byLifetime as $lifetime => $values) {
195+
try {
196+
$e = $this->doSave($values, $lifetime);
197+
} catch (\Exception $e) {
198+
}
199+
if (true === $e || [] === $e) {
200+
continue;
201+
}
202+
if (\is_array($e) || 1 === \count($values)) {
203+
foreach (\is_array($e) ? $e : array_keys($values) as $id) {
204+
$ok = false;
205+
$v = $values[$id];
206+
$type = \is_object($v) ? \get_class($v) : \gettype($v);
207+
CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', ['key' => substr($id, \strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null]);
208+
}
209+
} else {
210+
foreach ($values as $id => $v) {
211+
$retry[$lifetime][] = $id;
212+
}
213+
}
214+
}
215+
216+
// When bulk-save failed, retry each item individually
217+
foreach ($retry as $lifetime => $ids) {
218+
foreach ($ids as $id) {
219+
try {
220+
$v = $byLifetime[$lifetime][$id];
221+
$e = $this->doSave([$id => $v], $lifetime);
222+
} catch (\Exception $e) {
223+
}
224+
if (true === $e || [] === $e) {
225+
continue;
226+
}
227+
$ok = false;
228+
$type = \is_object($v) ? \get_class($v) : \gettype($v);
229+
CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', ['key' => substr($id, \strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null]);
230+
}
231+
}
232+
233+
return $ok;
234+
}
235+
236+
public function __destruct()
237+
{
238+
if ($this->deferred) {
239+
$this->commit();
240+
}
241+
}
242+
243+
private function generateItems($items, &$keys)
244+
{
245+
$f = $this->createCacheItem;
246+
247+
try {
248+
foreach ($items as $id => $value) {
249+
if (!isset($keys[$id])) {
250+
$id = key($keys);
251+
}
252+
$key = $keys[$id];
253+
unset($keys[$id]);
254+
yield $key => $f($key, $value, true);
255+
}
256+
} catch (\Exception $e) {
257+
CacheItem::log($this->logger, 'Failed to fetch requested items', ['keys' => array_values($keys), 'exception' => $e]);
258+
}
259+
260+
foreach ($keys as $key) {
261+
yield $key => $f($key, null, false);
262+
}
263+
}
264+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy