Skip to content

Commit 81a8ceb

Browse files
[Cache] Enable namespace-based invalidation by prefixing keys with backend-native namespace separators
1 parent 86c8a8a commit 81a8ceb

25 files changed

+357
-62
lines changed

composer.json

+2-2
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",
@@ -218,7 +218,7 @@
218218
"url": "src/Symfony/Contracts",
219219
"options": {
220220
"versions": {
221-
"symfony/contracts": "3.5.x-dev"
221+
"symfony/contracts": "3.6.x-dev"
222222
}
223223
}
224224
},

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

+5
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
use Symfony\Component\Yaml\Yaml;
209209
use Symfony\Contracts\Cache\CacheInterface;
210210
use Symfony\Contracts\Cache\CallbackInterface;
211+
use Symfony\Contracts\Cache\NamespacedPoolInterface;
211212
use Symfony\Contracts\Cache\TagAwareCacheInterface;
212213
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
213214
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -2576,6 +2577,10 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con
25762577
$container->registerAliasForArgument($tagAwareId, TagAwareCacheInterface::class, $pool['name'] ?? $name);
25772578
$container->registerAliasForArgument($name, CacheInterface::class, $pool['name'] ?? $name);
25782579
$container->registerAliasForArgument($name, CacheItemPoolInterface::class, $pool['name'] ?? $name);
2580+
2581+
if (interface_exists(NamespacedPoolInterface::class)) {
2582+
$container->registerAliasForArgument($name, NamespacedPoolInterface::class, $pool['name'] ?? $name);
2583+
}
25792584
}
25802585

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

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

+1-1
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

+18-5
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));
@@ -118,7 +131,7 @@ public static function createConnection(#[\SensitiveParameter] string $dsn, arra
118131
return MemcachedAdapter::createConnection($dsn, $options);
119132
}
120133
if (str_starts_with($dsn, 'couchbase:')) {
121-
if (class_exists('CouchbaseBucket') && CouchbaseBucketAdapter::isSupported()) {
134+
if (class_exists(\CouchbaseBucket::class) && CouchbaseBucketAdapter::isSupported()) {
122135
return CouchbaseBucketAdapter::createConnection($dsn, $options);
123136
}
124137

@@ -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

+28-10
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

+36-5
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

@@ -226,16 +228,38 @@ public function clear(string $prefix = ''): bool
226228
}
227229
}
228230

229-
if ($this->values) {
230-
return true;
231-
}
231+
return true;
232+
}
233+
234+
foreach ($this->subPools as $pool) {
235+
$pool->clear();
232236
}
233237

234-
$this->values = $this->tags = $this->expiries = [];
238+
$this->subPools = $this->values = $this->tags = $this->expiries = [];
235239

236240
return true;
237241
}
238242

243+
public function withSubNamespace(string $namespace): static
244+
{
245+
CacheItem::validateKey($namespace);
246+
247+
$subPools = $this->subPools;
248+
249+
if (isset($subPools[$namespace])) {
250+
return $subPools[$namespace];
251+
}
252+
253+
$this->subPools = [];
254+
$clone = clone $this;
255+
$clone->clear();
256+
257+
$subPools[$namespace] = $clone;
258+
$this->subPools = $subPools;
259+
260+
return $clone;
261+
}
262+
239263
/**
240264
* Returns all cached values, with cache miss as null.
241265
*/
@@ -263,6 +287,13 @@ public function reset(): void
263287
$this->clear();
264288
}
265289

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

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

+20-1
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

+3-3
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

+7-1
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;

0 commit comments

Comments
 (0)