Skip to content

Commit 93230d5

Browse files
authored
fix(cache): add support for cache with namespaces (#986)
1 parent 9e54447 commit 93230d5

File tree

9 files changed

+303
-16
lines changed

9 files changed

+303
-16
lines changed

src/Tracing/Cache/TraceableCacheAdapterForV3WithNamespace.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ public function withSubNamespace(string $namespace): static
5454

5555
$clone = clone $this;
5656
$clone->decoratedAdapter = $this->decoratedAdapter->withSubNamespace($namespace);
57+
$clone->namespace = null === $this->namespace
58+
? $namespace
59+
: \sprintf('%s.%s', $this->namespace, $namespace);
5760

5861
return $clone;
5962
}

src/Tracing/Cache/TraceableCacheAdapterTrait.php

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ trait TraceableCacheAdapterTrait
3434
*/
3535
private $decoratedAdapter;
3636

37+
/**
38+
* @var string|null
39+
*/
40+
protected $namespace;
41+
3742
/**
3843
* {@inheritdoc}
3944
*/
@@ -200,16 +205,23 @@ private function traceFunction(string $spanOperation, \Closure $callback, ?strin
200205
try {
201206
$result = $callback();
202207

203-
// Necessary for static analysis. Otherwise, the TResult type is assumed to be CacheItemInterface.
204-
if (!$result instanceof CacheItemInterface) {
205-
return $result;
208+
$data = [];
209+
210+
if ($result instanceof CacheItemInterface) {
211+
$data['cache.hit'] = $result->isHit();
212+
if ($result->isHit()) {
213+
$data['cache.item_size'] = static::getCacheItemSize($result->get());
214+
}
215+
}
216+
217+
$namespace = $this->getCacheNamespace();
218+
if (null !== $namespace) {
219+
$data['cache.namespace'] = $namespace;
206220
}
207221

208-
$data = ['cache.hit' => $result->isHit()];
209-
if ($result->isHit()) {
210-
$data['cache.item_size'] = static::getCacheItemSize($result->get());
222+
if ([] !== $data) {
223+
$span->setData($data);
211224
}
212-
$span->setData($data);
213225

214226
return $result;
215227
} finally {
@@ -282,10 +294,15 @@ private function traceGet(string $key, callable $callback, ?float $beta = null,
282294

283295
$now = microtime(true);
284296

285-
$getSpan->setData([
297+
$getData = [
286298
'cache.hit' => !$wasMiss,
287299
'cache.item_size' => self::getCacheItemSize($value),
288-
]);
300+
];
301+
$namespace = $this->getCacheNamespace();
302+
if (null !== $namespace) {
303+
$getData['cache.namespace'] = $namespace;
304+
}
305+
$getSpan->setData($getData);
289306

290307
// If we got a timestamp here we know that we missed
291308
if (null !== $saveStartTimestamp) {
@@ -296,9 +313,13 @@ private function traceGet(string $key, callable $callback, ?float $beta = null,
296313
->setDescription(urldecode($key));
297314
$saveSpan = $parentSpan->startChild($saveContext);
298315
$saveSpan->setStartTimestamp($saveStartTimestamp);
299-
$saveSpan->setData([
316+
$saveData = [
300317
'cache.item_size' => self::getCacheItemSize($value),
301-
]);
318+
];
319+
if (null !== $namespace) {
320+
$saveData['cache.namespace'] = $namespace;
321+
}
322+
$saveSpan->setData($saveData);
302323
$saveSpan->finish($now);
303324
} else {
304325
$getSpan->finish();
@@ -343,4 +364,12 @@ private function setCallbackWrapper(callable $callback, string $key): callable
343364
return $callback($this->decoratedAdapter->getItem($key));
344365
};
345366
}
367+
368+
/**
369+
* @return string|null
370+
*/
371+
protected function getCacheNamespace(): ?string
372+
{
373+
return $this->namespace;
374+
}
346375
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\SentryBundle\Tracing\Cache;
6+
7+
use Sentry\State\HubInterface;
8+
use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
9+
use Symfony\Component\Cache\PruneableInterface;
10+
use Symfony\Component\Cache\ResettableInterface;
11+
use Symfony\Contracts\Cache\NamespacedPoolInterface;
12+
use Symfony\Contracts\Cache\TagAwareCacheInterface;
13+
14+
/**
15+
* This implementation of a cache adapter aware of cache tags supports the
16+
* distributed tracing feature of Sentry.
17+
*
18+
* @internal
19+
*/
20+
final class TraceableTagAwareCacheAdapterForV3WithNamespace implements TagAwareAdapterInterface, TagAwareCacheInterface, NamespacedPoolInterface, PruneableInterface, ResettableInterface
21+
{
22+
/**
23+
* @phpstan-use TraceableCacheAdapterTrait<TagAwareAdapterInterface>
24+
*/
25+
use TraceableCacheAdapterTrait;
26+
27+
/**
28+
* @param HubInterface $hub The current hub
29+
* @param TagAwareAdapterInterface $decoratedAdapter The decorated cache adapter
30+
*/
31+
public function __construct(HubInterface $hub, TagAwareAdapterInterface $decoratedAdapter)
32+
{
33+
$this->hub = $hub;
34+
$this->decoratedAdapter = $decoratedAdapter;
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*
40+
* @param mixed[] $metadata
41+
*/
42+
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
43+
{
44+
return $this->traceGet($key, $callback, $beta, $metadata);
45+
}
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
public function invalidateTags(array $tags): bool
51+
{
52+
return $this->traceFunction('cache.invalidate_tags', function () use ($tags): bool {
53+
return $this->decoratedAdapter->invalidateTags($tags);
54+
});
55+
}
56+
57+
public function withSubNamespace(string $namespace): static
58+
{
59+
if (!$this->decoratedAdapter instanceof NamespacedPoolInterface) {
60+
throw new \BadMethodCallException(\sprintf('The %s::withSubNamespace() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, NamespacedPoolInterface::class));
61+
}
62+
63+
$clone = clone $this;
64+
$clone->decoratedAdapter = $this->decoratedAdapter->withSubNamespace($namespace);
65+
$clone->namespace = null === $this->namespace
66+
? $namespace
67+
: \sprintf('%s.%s', $this->namespace, $namespace);
68+
69+
return $clone;
70+
}
71+
}

src/aliases.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
use Sentry\SentryBundle\Tracing\Cache\TraceableCacheAdapter;
1010
use Sentry\SentryBundle\Tracing\Cache\TraceableCacheAdapterForV2;
1111
use Sentry\SentryBundle\Tracing\Cache\TraceableCacheAdapterForV3;
12-
use Sentry\SentryBundle\Tracing\Cache\TraceableCacheAdapterForV3WithNamespace;
1312
use Sentry\SentryBundle\Tracing\Cache\TraceableTagAwareCacheAdapter;
1413
use Sentry\SentryBundle\Tracing\Cache\TraceableTagAwareCacheAdapterForV2;
1514
use Sentry\SentryBundle\Tracing\Cache\TraceableTagAwareCacheAdapterForV3;
@@ -39,21 +38,24 @@
3938
use Symfony\Component\Cache\DoctrineProvider;
4039
use Symfony\Component\HttpClient\HttpClient;
4140
use Symfony\Component\HttpClient\Response\StreamableInterface;
42-
use Symfony\Contracts\Cache\NamespacedPoolInterface;
4341
use Symfony\Contracts\HttpClient\HttpClientInterface;
4442

4543
if (interface_exists(AdapterInterface::class)) {
4644
if (!class_exists(DoctrineProvider::class, false) && version_compare(\PHP_VERSION, '8.0.0', '>=')) {
4745
if (!class_exists(TraceableCacheAdapter::class, false)) {
48-
if (interface_exists(NamespacedPoolInterface::class)) {
49-
class_alias(TraceableCacheAdapterForV3WithNamespace::class, TraceableCacheAdapter::class);
46+
if (interface_exists('Symfony\\Contracts\\Cache\\NamespacedPoolInterface')) {
47+
class_alias('Sentry\\SentryBundle\\Tracing\\Cache\\TraceableCacheAdapterForV3WithNamespace', TraceableCacheAdapter::class);
5048
} else {
5149
class_alias(TraceableCacheAdapterForV3::class, TraceableCacheAdapter::class);
5250
}
5351
}
5452

5553
if (!class_exists(TraceableTagAwareCacheAdapter::class, false)) {
56-
class_alias(TraceableTagAwareCacheAdapterForV3::class, TraceableTagAwareCacheAdapter::class);
54+
if (interface_exists('Symfony\\Contracts\\Cache\\NamespacedPoolInterface')) {
55+
class_alias('Sentry\\SentryBundle\\Tracing\\Cache\\TraceableTagAwareCacheAdapterForV3WithNamespace', TraceableTagAwareCacheAdapter::class);
56+
} else {
57+
class_alias(TraceableTagAwareCacheAdapterForV3::class, TraceableTagAwareCacheAdapter::class);
58+
}
5759
}
5860
} else {
5961
if (!class_exists(TraceableCacheAdapter::class, false)) {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\SentryBundle\Tests\End2End\App\Controller;
6+
7+
use Symfony\Component\HttpFoundation\Response;
8+
use Symfony\Contracts\Cache\ItemInterface;
9+
use Symfony\Contracts\Cache\TagAwareCacheInterface;
10+
11+
class NamespacedCacheController
12+
{
13+
/**
14+
* @var TagAwareCacheInterface
15+
*/
16+
private $cache;
17+
18+
public function __construct(TagAwareCacheInterface $cache)
19+
{
20+
$this->cache = $cache;
21+
}
22+
23+
public function populateNamespacedCache(): Response
24+
{
25+
$namespaced = $this->cache->withSubNamespace('tests');
26+
27+
$namespaced->get('namespaced-key', function (ItemInterface $item) {
28+
$item->tag(['a tag name']);
29+
30+
return 'namespaced-value';
31+
});
32+
33+
return new Response();
34+
}
35+
}

tests/End2End/App/routing.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ psr_tracing_cache_delete:
7070
path: /tracing/cache/psr/delete
7171
defaults: { _controller: 'Sentry\SentryBundle\Tests\End2End\App\Controller\PsrTracingCacheController::testDelete' }
7272

73+
namespaced_tracing_cache_populate:
74+
path: /tracing/cache/namespaced/populate
75+
defaults: { _controller: 'Sentry\SentryBundle\Tests\End2End\App\Controller\NamespacedCacheController::populateNamespacedCache' }
76+
7377
just_logging:
7478
path: /just-logging
7579
defaults: { _controller: 'Sentry\SentryBundle\Tests\End2End\App\Controller\LoggingController::justLogging' }

tests/End2End/App/tracing.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,9 @@ services:
2323
autowire: true
2424
tags:
2525
- { name: controller.service_arguments }
26+
27+
Sentry\SentryBundle\Tests\End2End\App\Controller\NamespacedCacheController:
28+
arguments:
29+
$cache: '@cache.app.taggable'
30+
tags:
31+
- { name: controller.service_arguments }

tests/End2End/TracingCacheEnd2EndTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,35 @@ public function testPsrCacheDelete(): void
223223
$this->assertEquals('cache.remove', $span->getOp());
224224
$this->assertNull($span->getData('cache.item_size'));
225225
}
226+
227+
public function testNamespacedTagAwareCache(): void
228+
{
229+
if (!interface_exists(\Symfony\Contracts\Cache\NamespacedPoolInterface::class)) {
230+
$this->markTestSkipped('Namespaced caches are not supported by this Symfony version.');
231+
}
232+
233+
$client = static::createClient(['debug' => false]);
234+
$cache = static::getContainer()->get('cache.app.taggable');
235+
236+
// make sure that the configured taggable cache supports namespaces before running this test
237+
if (!$cache instanceof \Symfony\Contracts\Cache\NamespacedPoolInterface) {
238+
$this->markTestSkipped('The configured tag-aware cache pool does not support namespaces.');
239+
}
240+
241+
$client->request('GET', '/tracing/cache/namespaced/populate');
242+
$this->assertSame(200, $client->getResponse()->getStatusCode());
243+
244+
$this->assertCount(1, StubTransport::$events);
245+
$event = StubTransport::$events[0];
246+
247+
$cacheGetSpans = array_values(array_filter($event->getSpans(), static function ($span) {
248+
return 'cache.get' === $span->getOp();
249+
}));
250+
$this->assertNotEmpty($cacheGetSpans);
251+
252+
$cachePutSpans = array_filter($event->getSpans(), static function ($span) {
253+
return 'cache.put' === $span->getOp();
254+
});
255+
$this->assertNotEmpty($cachePutSpans);
256+
}
226257
}

0 commit comments

Comments
 (0)