8000 [Cache] Enable namespace-based invalidation by prefixing keys with ba… · symfony/symfony@0ca7660 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0ca7660

Browse files
[Cache] Enable namespace-based invalidation by prefixing keys with backend-native namespace separators
1 parent ca6f399 commit 0ca7660

25 files changed

+334
-49
lines changed

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"psr/http-message": "^1.0|^2.0",
4848
"psr/link": "^1.1|^2.0",
4949
"psr/log": "^1|^2|^3",
50-
"symfony/contracts": "^3.5",
50+
"symfony/contracts": "^3.6",
5151
"symfony/polyfill-ctype": "~1.8",
5252
"symfony/polyfill-intl-grapheme": "~1.0",
5353
"symfony/polyfill-intl-icu": "~1.0",
@@ -217,7 +217,7 @@
217217
"url": "src/Symfony/Contracts",
218218
"options": {
219219
"versions": {
220-
"symfony/contracts": "3.5.x-dev"
220+
"symfony/contracts": "3.6.x-dev"
221221
}
222222
}
223223
},

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@
207207
use Symfony\Component\Yaml\Yaml;
208208
use Symfony\Contracts\Cache\CacheInterface;
209209
use Symfony\Contracts\Cache\CallbackInterface;
210+
use Symfony\Contracts\Cache\NamespacedPoolInterface;
210211
use Symfony\Contracts\Cache\TagAwareCacheInterface;
211212
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
212213
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -2568,6 +2569,10 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con
25682569
$container->registerAliasForArgument($tagAwareId, TagAwareCacheInterface::class, $pool['name'] ?? $name);
25692570
$container->registerAliasForArgument($name, CacheInterface::class, $pool['name'] ?? $name);
25702571
$container->registerAliasForArgument($name, CacheItemPoolInterface::class, $pool['name'] ?? $name);
2572+
2573+
if (interface_exists(NamespacedPoolInterface::class)) {
2574+
$container->registerAliasForArgument($name, NamespacedPoolInterface::class, $pool['name'] ?? $name);
2575+
}
25712576
}
25722577

25732578
$definition->setPublic($pool['public']);

src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/cache.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
<tr>
9494
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
9595
<td class="nowrap">{{ '%0.2f'|format((call.end - call.start) * 1000) }} ms</td>
96-
<td class="nowrap">{{ call.name }}()</td>
96+
<td class="nowrap">{{ call.name }}({{ call.namespace|default:'' }})</td>
9797
<td>{{ profiler_dump(call.value.result, maxDepth=2) }}</td>
9898
</tr>
9999
{% endfor %}

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@
1919
use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
2020
use Symfony\Component\Cache\Traits\ContractsTrait;
2121
use Symfony\Contracts\Cache\CacheInterface;
22+
use Symfony\Contracts\Cache\NamespacedPoolInterface;
2223

2324
/**
2425
* @author Nicolas Grekas <p@tchwork.com>
2526
*/
26-
abstract class AbstractAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
27+
abstract class AbstractAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, LoggerAwareInterface, ResettableInterface
2728
{
2829
use AbstractAdapterTrait;
2930
use ContractsTrait;
@@ -37,7 +38,19 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
3738

3839
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
3940
{
40-
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::NS_SEPARATOR;
41+
if ('' !== $namespace) {
42+
if (str_contains($namespace, static::NS_SEPARATOR)) {
43+
if (str_contains($namespace, static::NS_SEPARATOR.static::NS_SEPARATOR)) {
44+
throw new InvalidArgumentException(\sprintf('Cache namespace "%s" contains empty sub-namespace.', $namespace));
45+
}
46+
CacheItem::validateKey(str_replace(static::NS_SEPARATOR, '', $namespace));
47+
} else {
48+
CacheItem::validateKey($namespace);
49+
}
50+
$this->namespace = $namespace.static::NS_SEPARATOR;
51+
}
52+
$this->rootNamespace = $this->namespace;
53+
4154
$this->defaultLifetime = $defaultLifetime;
4255
if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
4356
throw new InvalidArgumentException(\sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
@@ -159,7 +172,7 @@ public function commit(): bool
159172
$v = $values[$id];
160173
$type = get_debug_type($v);
161174
$message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
162-
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
175+
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
163176
}
164177
} else {
165178
foreach ($values as $id => $v) {
@@ -182,7 +195,7 @@ public function commit(): bool
182195
$ok = false;
183196
$type = get_debug_type($v);
184197
$message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
185-
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
198+
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
186199
}
187200
}
188201

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

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\Cache\ResettableInterface;
1818
use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
1919
use Symfony\Component\Cache\Traits\ContractsTrait;
20+
use Symfony\Contracts\Cache\NamespacedPoolInterface;
2021
use Symfony\Contracts\Cache\TagAwareCacheInterface;
2122

2223
/**
@@ -30,16 +31,33 @@
3031
*
3132
* @internal
3233
*/
33-
abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, LoggerAwareInterface, ResettableInterface
34+
abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, AdapterInterface, NamespacedPoolInterface, LoggerAwareInterface, ResettableInterface
3435
{
3536
use AbstractAdapterTrait;
3637
use ContractsTrait;
3738

39+
/**
40+
* @internal
41+
*/
42+
protected const NS_SEPARATOR = ':';
43+
3844
private const TAGS_PREFIX = "\1tags\1";
3945

4046
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
4147
{
42-
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
48+
if ('' !== $namespace) {
49+
if (str_contains($namespace, static::NS_SEPARATOR)) {
50+
if (str_contains($namespace, static::NS_SEPARATOR.static::NS_SEPARATOR)) {
51+
throw new InvalidArgumentException(\sprintf('Cache namespace "%s" contains empty sub-namespace.', $namespace));
52+
}
53+
CacheItem::validateKey(str_replace(static::NS_SEPARATOR, '', $namespace));
54+
} else {
55+
CacheItem::validateKey($namespace);
56+
}
57+
$this->namespace = $namespace.static::NS_SEPARATOR;
58+
}
59+
$this->rootNamespace = $this->namespace;
60+
4361
$this->defaultLifetime = $defaultLifetime;
4462
if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
4563
throw new InvalidArgumentException(\sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
@@ -70,7 +88,7 @@ static function ($key, $value, $isHit) {
7088
CacheItem::class
7189
);
7290
self::$mergeByLifetime ??= \Closure::bind(
73-
static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime) {
91+
static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime, $rootNamespace) {
7492
$byLifetime = [];
7593
$now = microtime(true);
7694
$expiredIds = [];
@@ -102,10 +120,10 @@ static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime)
102120
$value['tag-operations'] = ['add' => [], 'remove' => []];
103121
$oldTags = $item->metadata[CacheItem::METADATA_TAGS] ?? [];
104122
foreach (array_diff_key($value['tags'], $oldTags) as $addedTag) {
105-
$value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag);
123+
$value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag, $rootNamespace);
106124
}
107125
foreach (array_diff_key($oldTags, $value['tags']) as $removedTag) {
108-
$value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag);
126+
$value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag, $rootNamespace);
109127
}
110128
$value['tags'] = array_keys($value['tags']);
111129

@@ -168,7 +186,7 @@ protected function doDeleteYieldTags(array $ids): iterable
168186
public function commit(): bool
169187
{
170188
$ok = true;
171-
$byLifetime = (self::$mergeByLifetime)($this->deferred, $expiredIds, $this->getId(...), self::TAGS_PREFIX, $this->defaultLifetime);
189+
$byLifetime = (self::$mergeByLifetime)($this->deferred, $expiredIds, $this->getId(...), self::TAGS_PREFIX, $this->defaultLifetime, $this->rootNamespace);
172190
$retry = $this->deferred = [];
173191

174192
if ($expiredIds) {
@@ -195,7 +213,7 @@ public function commit(): bool
195213
$v = $values[$id];
196214
$type = get_debug_type($v);
197215
$message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
198-
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
216+
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
199217
}
200218
} else {
201219
foreach ($values as $id => $v) {
@@ -219,7 +237,7 @@ public function commit(): bool
219237
$ok = false;
220238
$type = get_debug_type($v);
221239
$message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
222-
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
240+
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
223241
}
224242
}
225243

@@ -244,7 +262,7 @@ public function deleteItems(array $keys): bool
244262
try {
245263
foreach ($this->doDeleteYieldTags(array_values($ids)) as $id => $tags) {
246264
foreach ($tags as $tag) {
247-
$tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id;
265+
$tagData[$this->getId(self::TAGS_PREFIX.$tag, $this->rootNamespace)][] = $id;
248266
}
249267
}
250268
} catch (\Exception) {
@@ -283,7 +301,7 @@ public function invalidateTags(array $tags): bool
283301

284302
$tagIds = [];
285303
foreach (array_unique($tags) as $tag) {
286-
$tagIds[] = $this->getId(self::TAGS_PREFIX.$tag);
304+
$tagIds[] = $this->getId(self::TAGS_PREFIX.$tag, $this->rootNamespace);
287305
}
288306

289307
try {

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

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\Cache\Exception\InvalidArgumentException;
2020
use Symfony\Component\Cache\ResettableInterface;
2121
use Symfony\Contracts\Cache\CacheInterface;
22+
use Symfony\Contracts\Cache\NamespacedPoolInterface;
2223

2324
/**
2425
* An in-memory cache storage.
@@ -27,13 +28,14 @@
2728
*
2829
* @author Nicolas Grekas <p@tchwork.com>
2930
*/
30-
class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
31+
class ArrayAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, LoggerAwareInterface, ResettableInterface
3132
{
3233
use LoggerAwareTrait;
3334

3435
private array $values = [];
3536
private array $tags = [];
3637
private array $expiries = [];
38+
private array $subPools = [];
3739

3840
private static \Closure $createCacheItem;
3941

@@ -231,11 +233,31 @@ public function clear(string $prefix = ''): bool
231233
}
232234
}
233235

234-
$this->values = $this->tags = $this->expiries = [];
236+
$this->subPools = $this->values = $this->tags = $this->expiries = [];
235237

236238
return true;
237239
}
238240

241+
public function withSubNamespace(string $namespace): static
242+
{
243+
CacheItem::validateKey($namespace);
244+
245+
$subPools = $this->subPools;
246+
247+
if (isset($subPools[$namespace])) {
248+
return $subPools[$namespace];
249+
}
250+
251+
$this->subPools = [];
252+
$clone = clone $this;
253+
$clone->clear();
254+
255+
$subPools[$namespace] = $clone;
256+
$this->subPools = $subPools;
257+
258+
return $clone;
259+
}
260+
239261
/**
240262
* Returns all cached values, with cache miss as null.
241263
*/
@@ -263,6 +285,13 @@ public function reset(): void
263285
$this->clear();
264286
}
265287

288+
public function __clone()
289+
{
290+
foreach ($this->subPools as $i => $pool) {
291+
$this->subPools[$i] = clone $pool;
292+
}
293+
}
294+
266295
private function generateItems(array $keys, float $now, \Closure $f): \Generator
267296
{
268297
foreach ($keys as $i => $key) {

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
use Psr\Cache\CacheItemInterface;
1515
use Psr\Cache\CacheItemPoolInterface;
1616
use Symfony\Component\Cache\CacheItem;
17+
use Symfony\Component\Cache\Exception\BadMethodCallException;
1718
use Symfony\Component\Cache\Exception\InvalidArgumentException;
1819
use Symfony\Component\Cache\PruneableInterface;
1920
use Symfony\Component\Cache\ResettableInterface;
2021
use Symfony\Component\Cache\Traits\ContractsTrait;
2122
use Symfony\Contracts\Cache\CacheInterface;
23+
use Symfony\Contracts\Cache\NamespacedPoolInterface;
2224
use Symfony\Contracts\Service\ResetInterface;
2325

2426
/**
@@ -29,7 +31,7 @@
2931
*
3032
* @author Kévin Dunglas <dunglas@gmail.com>
3133
*/
32-
class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
34+
class ChainAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, PruneableInterface, ResettableInterface
3335
{
3436
use ContractsTrait;
3537

@@ -280,6 +282,23 @@ public function prune(): bool
280282
return $pruned;
281283
}
282284

285+
public function withSubNamespace(string $namespace): static
286+
{
287+
$clone = clone $this;
288+
$adapters = [];
289+
290+
foreach ($this->adapters as $adapter) {
291+
if (!$adapter instanceof NamespacedPoolInterface) {
292+
throw new BadMethodCallException('All adapters must implement NamespacedPoolInterface to support namespaces.');
293+
}
294+
295+
$adapters[] = $adapter->withSubNamespace($namespace);
296+
}
297+
$clone->adapters = $adapters;
298+
299+
return $clone;
300+
}
301+
283302
public function reset(): void
284303
{
285304
foreach ($this->adapters as $adapter) {

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -335,17 +335,17 @@ protected function doSave(array $values, int $lifetime): array|bool
335335
/**
336336
* @internal
337337
*/
338-
protected function getId(mixed $key): string
338+
protected function getId(mixed $key, ?string $namespace = null): string
339339
{
340340
if ('pgsql' !== $this->platformName ??= $this->getPlatformName()) {
341-
return parent::getId($key);
341+
return parent::getId($key, $namespace);
342342
}
343343

344344
if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
345345
$key = rawurlencode($key);
346346
}
347347

348-
return parent::getId($key);
348+
return parent::getId($key, $namespace);
349349
}
350350

351351
private function getPlatformName(): string

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
use Psr\Cache\CacheItemInterface;
1515
use Symfony\Component\Cache\CacheItem;
1616
use Symfony\Contracts\Cache\CacheInterface;
17+
use Symfony\Contracts\Cache\NamespacedPoolInterface;
1718

1819
/**
1920
* @author Titouan Galopin <galopintitouan@gmail.com>
2021
*/
21-
class NullAdapter implements AdapterInterface, CacheInterface
22+
class NullAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface
2223
{
2324
private static \Closure $createCacheItem;
2425

@@ -94,6 +95,11 @@ public function delete(string $key): bool
9495
return $this->deleteItem($key);
9596
}
9697

98+
public function withSubNamespace(string $namespace): static
99+
{
100+
return clone $this;
101+
}
102+
97103
private function generateItems(array $keys): \Generator
98104
{
99105
$f = self::$createCacheItem;

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,17 +348,17 @@ protected function doSave(array $values, int $lifetime): array|bool
348348
/**
349349
* @internal
350350
*/
351-
protected function getId(mixed $key): string
351+
protected function getId(mixed $key, ?string $namespace = null): string
352352
{
353353
if ('pgsql' !== $this->getDriver()) {
354-
return parent::getId($key);
354+
return parent::getId($key, $namespace);
355355
}
356356

357357
if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
358358
$key = rawurlencode($key);
359359
}
360360

361-
return parent::getId($key);
361+
return parent::getId($key, $namespace);
362362
}
363363

364364
private function getConnection(): \PDO

0 commit comments

Comments
 (0)
0