Skip to content

Commit b029f52

Browse files
[Cache] serialize tags separately from values in AbstractTagAwareAdapter
1 parent 6e7f325 commit b029f52

File tree

6 files changed

+179
-11
lines changed

6 files changed

+179
-11
lines changed

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

+12-2
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,16 @@ abstract protected function doDelete(array $ids, array $tagData = []): bool;
149149
*/
150150
abstract protected function doInvalidate(array $tagIds): bool;
151151

152+
/**
153+
* Returns the tags bound to the provided ids.
154+
*/
155+
protected function doFetchTags(array $ids): iterable
156+
{
157+
foreach ($this->doFetch($ids) as $id => $value) {
158+
yield $id => \is_array($value) && \is_array($value['tags'] ?? null) ? $value['tags'] : [];
159+
}
160+
}
161+
152162
/**
153163
* {@inheritdoc}
154164
*
@@ -233,8 +243,8 @@ public function deleteItems(array $keys)
233243
}
234244

235245
try {
236-
foreach ($this->doFetch($ids) as $id => $value) {
237-
foreach ($value['tags'] ?? [] as $tag) {
246+
foreach ($this->doFetchTags($ids) as $id => $tags) {
247+
foreach ($tags as $tag) {
238248
$tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id;
239249
}
240250
}

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

+36-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111

1212
namespace Symfony\Component\Cache\Adapter;
1313

14-
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
1514
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
15+
use Symfony\Component\Cache\Marshaller\TagAwareMarshaller;
1616
use Symfony\Component\Cache\PruneableInterface;
1717
use Symfony\Component\Cache\Traits\FilesystemTrait;
1818

@@ -37,7 +37,7 @@ class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements Prune
3737

3838
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, MarshallerInterface $marshaller = null)
3939
{
40-
$this->marshaller = $marshaller ?? new DefaultMarshaller();
40+
$this->marshaller = new TagAwareMarshaller($marshaller);
4141
parent::__construct('', $defaultLifetime);
4242
$this->init($namespace, $directory);
4343
}
@@ -130,6 +130,40 @@ protected function doSave(array $values, ?int $lifetime, array $addTagData = [],
130130
return $failed;
131131
}
132132

133+
/**
134+
* {@inheritdoc}
135+
*/
136+
protected function doFetchTags(array $ids): iterable
137+
{
138+
foreach ($ids as $id) {
139+
$file = $this->getFile($id);
140+
if (!file_exists($file) || !$h = @fopen($file, 'rb')) {
141+
continue;
142+
}
143+
144+
$meta = explode("\n", fread($h, 4096), 3)[2] ?? '';
145+
146+
// detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
147+
if (13 < \strlen($meta) && "\x9D" === $meta[0] && "\0" === $meta[5] && "\x5F" === $meta[9]) {
148+
$meta[9] = "\0";
149+
$tagLen = unpack('Nlen', $meta, 9)['len'];
150+
$meta = substr($meta, 13, $tagLen);
151+
152+
if (0 < $tagLen -= \strlen($meta)) {
153+
$meta .= fread($h, $tagLen);
154+
}
155+
156+
try {
157+
yield $id => '' === $meta ? [] : $this->marshaller->unmarshall($meta);
158+
} catch (\Exception $e) {
159+
yield $id => [];
160+
}
161+
}
162+
163+
fclose($h);
164+
}
165+
}
166+
133167
/**
134168
* {@inheritdoc}
135169
*/

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

+38-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Predis\Response\Status;
1717
use Symfony\Component\Cache\Exception\InvalidArgumentException;
1818
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
19+
use Symfony\Component\Cache\Marshaller\TagAwareMarshaller;
1920
use Symfony\Component\Cache\Traits\RedisTrait;
2021

2122
/**
@@ -67,7 +68,7 @@ public function __construct($redisClient, string $namespace = '', int $defaultLi
6768
throw new InvalidArgumentException(sprintf('Unsupported Predis cluster connection: only "%s" is, "%s" given.', PredisCluster::class, \get_class($redisClient->getConnection())));
6869
}
6970

70-
$this->init($redisClient, $namespace, $defaultLifetime, $marshaller);
71+
$this->init($redisClient, $namespace, $defaultLifetime, new TagAwareMarshaller($marshaller));
7172
}
7273

7374
/**
@@ -119,6 +120,42 @@ protected function doSave(array $values, ?int $lifetime, array $addTagData = [],
119120
return $failed;
120121
}
121122

123+
/**
124+
* {@inheritdoc}
125+
*/
126+
protected function doFetchTags(array $ids): iterable
127+
{
128+
$lua = <<<'EOLUA'
129+
local v = redis.call('GET', KEYS[1])
130+
131+
if not v or v:len() <= 13 or v:byte(1) ~= 0x9D or v:byte(6) ~= 0 or v:byte(10) ~= 0x5F then
132+
return ''
133+
end
134+
135+
return v:sub(14, 13 + v:byte(13) + v:byte(12) * 256 + v:byte(11) * 65536)
136+
EOLUA;
137+
138+
if ($this->redis instanceof \Predis\ClientInterface) {
139+
$evalArgs = [$lua, 1, &$id];
140+
} else {
141+
$evalArgs = [$lua, [&$id], 1];
142+
}
143+
144+
$results = $this->pipeline(function () use ($ids, &$id, $evalArgs) {
145+
foreach ($ids as $id) {
146+
yield 'eval' => $evalArgs;
147+
}
148+
});
149+
150+
foreach ($results as $id => $result) {
151+
try {
152+
yield $id => !\is_string($result) || '' === $result ? [] : $this->marshaller->unmarshall($result);
153+
} catch (\Exception $e) {
154+
yield $id => [];
155+
}
156+
}
157+
}
158+
122159
/**
123160
* {@inheritdoc}
124161
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
namespace Symfony\Component\Cache\Marshaller;
13+
14+
/**
15+
* A marshaller optimized for data structures generated by AbstractTagAwareAdapter.
16+
*
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
class TagAwareMarshaller implements MarshallerInterface
20+
{
21+
private $marshaller;
22+
23+
public function __construct(MarshallerInterface $marshaller = null)
24+
{
25+
$this->marshaller = $marshaller ?? new DefaultMarshaller();
26+
}
27+
28+
/**
29+
* {@inheritdoc}
30+
*/
31+
public function marshall(array $values, ?array &$failed): array
32+
{
33+
$failed = $notSerialized = $serialized = [];
34+
35+
foreach ($values as $id => $value) {
36+
if (\is_array($value) && \is_array($value['tags'] ?? null) && \array_key_exists('value', $value) && \count($value) === 2 + (\is_string($value['meta'] ?? null) && 8 === \strlen($value['meta']))) {
37+
// if the value is an array with keys "tags", "value" and "meta", use a compact serialization format
38+
// magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F allow detecting this format quickly in unmarshall()
39+
40+
$v = $this->marshaller->marshall($value, $f);
41+
42+
if ($f) {
43+
$f = [];
44+
} else {
45+
if ([] === $value['tags']) {
46+
$v['tags'] = '';
47+
}
48+
49+
$serialized[$id] = "\x9D".($value['meta'] ?? "\0\0\0\0\0\0\0\0").pack('N', \strlen($v['tags'])).$v['tags'].$v['value'];
50+
$serialized[$id][9] = "\x5F";
51+
continue;
52+
}
53+
}
54+
55+
// other arbitratry values are serialized using the decorated marshaller below
56+
$notSerialized[$id] = $value;
57+
}
58+
59+
return $notSerialized ? $serialized + $this->marshaller->marshall($notSerialized, $failed) : $serialized;
60+
}
61+
62+
/**
63+
* {@inheritdoc}
64+
*/
65+
public function unmarshall(string $value)
66+
{
67+
// detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
68+
if (13 >= \strlen($value) || "\x9D" !== $value[0] || "\0" !== $value[5] || "\x5F" !== $value[9]) {
69+
return $this->marshaller->unmarshall($value);
70+
}
71+
72+
// data consists of value, tags and metadata which we need to unpack
73+
$meta = substr($value, 1, 12);
74+
$meta[8] = "\0";
75+
$tagLen = unpack('Nlen', $meta, 8)['len'];
76+
$meta = substr($meta, 0, 8);
77+
78+
return [
79+
'value' => $this->marshaller->unmarshall(substr($value, 13 + $tagLen)),
80+
'tags' => $tagLen ? $this->marshaller->unmarshall(substr($value, 13, $tagLen)) : [],
81+
'meta' => "\0\0\0\0\0\0\0\0" === $meta ? null : $meta,
82+
];
83+
}
84+
}

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,13 @@ public function getItem($key)
5151
foreach ($this->doFetch([$id]) as $value) {
5252
$isHit = true;
5353
}
54+
55+
return $f($key, $value, $isHit);
5456
} catch (\Exception $e) {
5557
CacheItem::log($this->logger, 'Failed to fetch key "{key}": '.$e->getMessage(), ['key' => $key, 'exception' => $e]);
5658
}
5759

58-
return $f($key, $value, $isHit);
60+
return $f($key, null, false);
5961
}
6062

6163
/**

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

+6-5
Original file line numberDiff line numberDiff line change
@@ -445,25 +445,26 @@ private function pipeline(\Closure $generator, $redis = null): \Generator
445445
$results = [];
446446
foreach ($generator() as $command => $args) {
447447
$results[] = $redis->{$command}(...$args);
448-
$ids[] = $args[0];
448+
$ids[] = 'eval' === $command ? ($redis instanceof \Predis\ClientInterface ? $args[2] : $args[1][0]) : $args[0];
449449
}
450450
} elseif ($redis instanceof \Predis\ClientInterface) {
451451
$results = $redis->pipeline(static function ($redis) use ($generator, &$ids) {
452452
foreach ($generator() as $command => $args) {
453453
$redis->{$command}(...$args);
454-
$ids[] = $args[0];
454+
$ids[] = 'eval' === $command ? $args[2] : $args[0];
455455
}
456456
});
457457
} elseif ($redis instanceof \RedisArray) {
458458
$connections = $results = $ids = [];
459459
foreach ($generator() as $command => $args) {
460-
if (!isset($connections[$h = $redis->_target($args[0])])) {
460+
$id = 'eval' === $command ? $args[1][0] : $args[0];
461+
if (!isset($connections[$h = $redis->_target($id)])) {
461462
$connections[$h] = [$redis->_instance($h), -1];
462463
$connections[$h][0]->multi(\Redis::PIPELINE);
463464
}
464465
$connections[$h][0]->{$command}(...$args);
465466
$results[] = [$h, ++$connections[$h][1]];
466-
$ids[] = $args[0];
467+
$ids[] = $id;
467468
}
468469
foreach ($connections as $h => $c) {
469470
$connections[$h] = $c[0]->exec();
@@ -475,7 +476,7 @@ private function pipeline(\Closure $generator, $redis = null): \Generator
475476
$redis->multi(\Redis::PIPELINE);
476477
foreach ($generator() as $command => $args) {
477478
$redis->{$command}(...$args);
478-
$ids[] = $args[0];
479+
$ids[] = 'eval' === $command ? $args[1][0] : $args[0];
479480
}
480481
$results = $redis->exec();
481482
}

0 commit comments

Comments
 (0)