Skip to content

Commit 5880f9b

Browse files
[Cache] add integration with Messenger to allow computing cached values in a worker
1 parent 908ca44 commit 5880f9b

18 files changed

+557
-29
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,9 @@ private function addCacheSection(ArrayNodeDefinition $rootNode)
10411041
->scalarNode('provider')
10421042
->info('Overwrite the setting from the default provider for this adapter.')
10431043
->end()
1044+
->scalarNode('early_expiration_message_bus')
1045+
->example('"messenger.default_bus" to send early expiration events to the default Messenger bus.')
1046+
->end()
10441047
->scalarNode('clearer')->end()
10451048
->end()
10461049
->end()

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@
147147
use Symfony\Component\Yaml\Command\LintCommand as BaseYamlLintCommand;
148148
use Symfony\Component\Yaml\Yaml;
149149
use Symfony\Contracts\Cache\CacheInterface;
150+
use Symfony\Contracts\Cache\CallbackInterface;
150151
use Symfony\Contracts\Cache\TagAwareCacheInterface;
151152
use Symfony\Contracts\HttpClient\HttpClientInterface;
152153
use Symfony\Contracts\Service\ResetInterface;
@@ -436,6 +437,8 @@ public function load(array $configs, ContainerBuilder $container)
436437
->addTag('container.env_var_loader');
437438
$container->registerForAutoconfiguration(EnvVarProcessorInterface::class)
438439
->addTag('container.env_var_processor');
440+
$container->registerForAutoconfiguration(CallbackInterface::class)
441+
->addTag('container.reversible');
439442
$container->registerForAutoconfiguration(ServiceLocator::class)
440443
->addTag('container.service_locator');
441444
$container->registerForAutoconfiguration(ServiceSubscriberInterface::class)

src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
2626
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
2727
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
28+
use Symfony\Component\Cache\Messenger\EarlyExpirationHandler;
2829
use Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer;
2930
use Symfony\Contracts\Cache\CacheInterface;
3031
use Symfony\Contracts\Cache\TagAwareCacheInterface;
@@ -212,6 +213,12 @@
212213
null, // use igbinary_serialize() when available
213214
])
214215

216+
->set('cache.early_expiration_handler', EarlyExpirationHandler::class)
217+
->args([
218+
service('reverse_container'),
219+
])
220+
->tag('messenger.message_handler')
221+
215222
->set('cache.default_clearer', Psr6CacheClearer::class)
216223
->args([
217224
[],

src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@
284284
<xsd:attribute name="public" type="xsd:boolean" />
285285
<xsd:attribute name="default-lifetime" type="xsd:integer" />
286286
<xsd:attribute name="provider" type="xsd:string" />
287+
<xsd:attribute name="early-expiration-message-bus" type="xsd:string" />
287288
<xsd:attribute name="clearer" type="xsd:string" />
288289
</xsd:complexType>
289290

src/Symfony/Component/Cache/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.2.0
5+
-----
6+
7+
* added integration with Messenger to allow computing cached values in a worker
8+
49
5.1.0
510
-----
611

src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,21 @@ class CachePoolPass implements CompilerPassInterface
3232
private $cachePoolClearerTag;
3333
private $cacheSystemClearerId;
3434
private $cacheSystemClearerTag;
35+
private $reverseContainerId;
36+
private $reversibleTag;
37+
private $messageHandlerId;
3538

36-
public function __construct(string $cachePoolTag = 'cache.pool', string $kernelResetTag = 'kernel.reset', string $cacheClearerId = 'cache.global_clearer', string $cachePoolClearerTag = 'cache.pool.clearer', string $cacheSystemClearerId = 'cache.system_clearer', string $cacheSystemClearerTag = 'kernel.cache_clearer')
39+
public function __construct(string $cachePoolTag = 'cache.pool', string $kernelResetTag = 'kernel.reset', string $cacheClearerId = 'cache.global_clearer', string $cachePoolClearerTag = 'cache.pool.clearer', string $cacheSystemClearerId = 'cache.system_clearer', string $cacheSystemClearerTag = 'kernel.cache_clearer', string $reverseContainerId = 'reverse_container', string $reversibleTag = 'container.reversible', string $messageHandlerId = 'cache.early_expiration_handler')
3740
{
3841
$this->cachePoolTag = $cachePoolTag;
3942
$this->kernelResetTag = $kernelResetTag;
4043
$this->cacheClearerId = $cacheClearerId;
4144
$this->cachePoolClearerTag = $cachePoolClearerTag;
4245
$this->cacheSystemClearerId = $cacheSystemClearerId;
4346
$this->cacheSystemClearerTag = $cacheSystemClearerTag;
47+
$this->reverseContainerId = $reverseContainerId;
48+
$this->reversibleTag = $reversibleTag;
49+
$this->messageHandlerId = $messageHandlerId;
4450
}
4551

4652
/**
@@ -55,13 +61,15 @@ public function process(ContainerBuilder $container)
5561
$seed .= '.'.$container->getParameter('kernel.container_class');
5662
}
5763

64+
$needsMessageHandler = false;
5865
$allPools = [];
5966
$clearers = [];
6067
$attributes = [
6168
'provider',
6269
'name',
6370
'namespace',
6471
'default_lifetime',
72+
'early_expiration_message_bus',
6573
'reset',
6674
];
6775
foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $tags) {
@@ -155,13 +163,24 @@ public function process(ContainerBuilder $container)
155163
if ($tags[0][$attr]) {
156164
$pool->addTag($this->kernelResetTag, ['method' => $tags[0][$attr]]);
157165
}
166+
} elseif ('early_expiration_message_bus' === $attr) {
167+
$needsMessageHandler = true;
168+
$pool->addMethodCall('setCallbackWrapper', [(new Definition(Dispatcher::class))
169+
->addArgument(new Reference($tags[0]['early_expiration_message_bus']))
170+
->addArgument(new Reference($this->reverseContainerId))
171+
->addArgument((new Definition('callable'))
172+
->setFactory([new Reference($id), 'setCallbackWrapper'])
173+
->addArgument(null)
174+
),
175+
]);
176+
$pool->addTag($this->reversibleTag);
158177
} elseif ('namespace' !== $attr || ArrayAdapter::class !== $class) {
159178
$pool->replaceArgument($i++, $tags[0][$attr]);
160179
}
161180
unset($tags[0][$attr]);
162181
}
163182
if (!empty($tags[0])) {
164-
throw new InvalidArgumentException(sprintf('Invalid "%s" tag for service "%s": accepted attributes are "clearer", "provider", "name", "namespace", "default_lifetime" and "reset", found "%s".', $this->cachePoolTag, $id, implode('", "', array_keys($tags[0]))));
183+
throw new InvalidArgumentException(sprintf('Invalid "%s" tag for service "%s": accepted attributes are "clearer", "provider", "name", "namespace", "default_lifetime", "early_expiration_message_bus" and "reset", found "%s".', $this->cachePoolTag, $id, implode('", "', array_keys($tags[0]))));
165184
}
166185

167186
if (null !== $clearer) {
@@ -171,6 +190,10 @@ public function process(ContainerBuilder $container)
171190
$allPools[$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE);
172191
}
173192

193+
if (!$needsMessageHandler) {
194+
$container->removeDefinition($this->messageHandlerId);
195+
}
196+
174197
$notAliasedCacheClearerId = $this->cacheClearerId;
175198
while ($container->hasAlias($this->cacheClearerId)) {
176199
$this->cacheClearerId = (string) $container->getAlias($this->cacheClearerId);
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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\Messenger;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\Cache\Adapter\AdapterInterface;
16+
use Symfony\Component\Cache\CacheItem;
17+
use Symfony\Component\DependencyInjection\ReverseContainer;
18+
use Symfony\Component\Messenger\MessageBusInterface;
19+
use Symfony\Component\Messenger\Stamp\HandledStamp;
20+
21+
/**
22+
* Sends the computation of cached values to a message bus.
23+
*/
24+
class EarlyExpirationDispatcher
25+
{
26+
private $bus;
27+
private $reverseContainer;
28+
private $callbackWrapper;
29+
30+
public function __construct(MessageBusInterface $bus, ReverseContainer $reverseContainer, callable $callbackWrapper = null)
31+
{
32+
$this->bus = $bus;
33+
$this->reverseContainer = $reverseContainer;
34+
$this->callbackWrapper = $callbackWrapper;
35+
}
36+
37+
public function __invoke(callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, LoggerInterface $logger = null)
38+
{
39+
if (!$item->isHit() || null === $message = EarlyExpirationMessage::create($this->reverseContainer, $callback, $item, $pool)) {
40+
// The item is stale or the callback cannot be reversed: we must compute the value now
41+
$logger && $logger->info('Computing item "{key}" online: '.($item->isHit() ? 'callback cannot be reversed' : 'item is stale'), ['key' => $item->getKey()]);
42+
43+
return null !== $this->callbackWrapper ? ($this->callbackWrapper)($callback, $item, $save, $pool, $setMetadata, $logger) : $callback($item, $save);
44+
}
45+
46+
$envelope = $this->bus->dispatch($message);
47+
48+
if ($logger) {
49+
if ($envelope->last(HandledStamp::class)) {
50+
$logger->info('Item "{key}" was computed online', ['key' => $item->getKey()]);
51+
} else {
52+
$logger->info('Item "{key}" sent for recomputation', ['key' => $item->getKey()]);
53+
}
54+
}
55+
56+
// The item's value is not stale, no need to write it to the backend
57+
$save = false;
58+
59+
return $message->getItem()->get() ?? $item->get();
60+
}
61+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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\Messenger;
13+
14+
use Symfony\Component\Cache\CacheItem;
15+
use Symfony\Component\DependencyInjection\ReverseContainer;
16+
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
17+
18+
/**
19+
* Computes cached values sent to a message bus.
20+
*/
21+
class EarlyExpirationHandler implements MessageHandlerInterface
22+
{
23+
private $reverseContainer;
24+
private $processedNonces = [];
25+
26+
public function __construct(ReverseContainer $reverseContainer)
27+
{
28+
$this->reverseContainer = $reverseContainer;
29+
}
30+
31+
public function __invoke(EarlyExpirationMessage $message)
32+
{
33+
$item = $message->getItem();
34+
$metadata = $item->getMetadata();
35+
$expiry = $metadata[CacheItem::METADATA_EXPIRY] ?? 0;
36+
$ctime = $metadata[CacheItem::METADATA_CTIME] ?? 0;
37+
38+
if ($expiry && $ctime) {
39+
// skip duplicate or expired messages
40+
41+
$processingNonce = [$expiry, $ctime];
42+
$pool = $message->getPool();
43+
$key = $item->getKey();
44+
45+
if (($this->processedNonces[$pool][$key] ?? null) === $processingNonce) {
46+
return;
47+
}
48+
49+
if (microtime(true) >= $expiry) {
50+
return;
51+
}
52+
53+
$this->processedNonces[$pool] = [$key => $processingNonce] + ($this->processedNonces[$pool] ?? []);
54+
55+
if (\count($this->processedNonces[$pool]) > 100) {
56+
array_pop($this->processedNonces[$pool]);
57+
}
58+
}
59+
60+
static $setMetadata;
61+
62+
$setMetadata = $setMetadata ?? \Closure::bind(
63+
function (CacheItem $item, float $startTime) {
64+
if ($item->expiry > $endTime = microtime(true)) {
65+
$item->newMetadata[CacheItem::METADATA_EXPIRY] = $item->expiry;
66+
$item->newMetadata[CacheItem::METADATA_CTIME] = (int) ceil(1000 * ($endTime - $startTime));
67+
}
68+
},
69+
null,
70+
CacheItem::class
71+
);
72+
73+
$startTime = microtime(true);
74+
$pool = $message->findPool($this->reverseContainer);
75+
$callback = $message->findCallback($this->reverseContainer);
76+
$value = $callback($item);
77+
$setMetadata($item, $startTime);
78+
$pool->save($item->set($value));
79+
}
80+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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\Messenger;
13+
14+
use Symfony\Component\Cache\Adapter\AdapterInterface;
15+
use Symfony\Component\Cache\CacheItem;
16+
use Symfony\Component\DependencyInjection\ReverseContainer;
17+
18+
/**
19+
* Conveys a cached value that needs to be computed.
20+
*/
21+
final class EarlyExpirationMessage
22+
{
23+
private $item;
24+
private $pool;
25+
private $callback;
26+
27+
public static function create(ReverseContainer $reverseContainer, callable $callback, CacheItem $item, AdapterInterface $pool): ?self
28+
{
29+
try {
30+
$item = clone $item;
31+
$item->set(null);
32+
} catch (\Exception $e) {
33+
return null;
34+
}
35+
36+
$pool = $reverseContainer->getId($pool);
37+
38+
if (\is_object($callback)) {
39+
if (null === $id = $reverseContainer->getId($callback)) {
40+
return null;
41+
}
42+
43+
$callback = '@'.$id;
44+
} elseif (!\is_array($callback)) {
45+
$callback = (string) $callback;
46+
} elseif (!\is_object($callback[0])) {
47+
$callback = [(string) $callback[0], (string) $callback[1]];
48+
} else {
49+
if (null === $id = $reverseContainer->getId($callback[0])) {
50+
return null;
51+
}
52+
53+
$callback = ['@'.$id, (string) $callback[1]];
54+
}
55+
56+
return new self($item, $pool, $callback);
57+
}
58+
59+
public function getItem(): CacheItem
60+
{
61+
return $this->item;
62+
}
63+
64+
public function getPool(): string
65+
{
66+
return $this->pool;
67+
}
68+
69+
public function getCallback()
70+
{
71+
return $this->callback;
72+
}
73+
74+
public function findPool(ReverseContainer $reverseContainer): AdapterInterface
75+
{
76+
return $reverseContainer->getService($this->pool);
77+
}
78+
79+
public function findCallback(ReverseContainer $reverseContainer): callable
80+
{
81+
if (\is_string($callback = $this->callback)) {
82+
return '@' === $callback[0] ? $reverseContainer->getService(substr($callback, 1)) : $callback;
83+
}
84+
if ('@' === $callback[0][0]) {
85+
$callback[0] = $reverseContainer->getService(substr($callback[0], 1));
86+
}
87+
88+
return $callback;
89+
}
90+
91+
private function __construct(CacheItem $item, string $pool, $callback)
92+
{
93+
$this->item = $item;
94+
$this->pool = $pool;
95+
$this->callback = $callback;
96+
}
97+
}

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