From 6c0911f58c56da337695feedde09f62e8ebcbe9f Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 24 Oct 2018 11:17:56 +0200 Subject: [PATCH] [Cache] add integration with Messenger to allow computing cached values in a worker --- .../DependencyInjection/Configuration.php | 3 + .../FrameworkExtension.php | 3 + .../Resources/config/cache.php | 7 + .../Resources/config/schema/symfony-1.0.xsd | 1 + src/Symfony/Component/Cache/CHANGELOG.md | 5 + .../DependencyInjection/CachePoolPass.php | 28 +++- .../Messenger/EarlyExpirationDispatcher.php | 61 ++++++++ .../Messenger/EarlyExpirationHandler.php | 80 +++++++++++ .../Messenger/EarlyExpirationMessage.php | 97 +++++++++++++ .../Tests/Adapter/FilesystemAdapterTest.php | 25 +--- .../Tests/Adapter/PhpArrayAdapterTest.php | 3 +- .../PhpArrayAdapterWithFallbackTest.php | 3 +- .../Tests/Adapter/PhpFilesAdapterTest.php | 3 +- .../Tests/Adapter/TagAwareAdapterTest.php | 3 +- .../EarlyExpirationDispatcherTest.php | 134 ++++++++++++++++++ .../Messenger/EarlyExpirationHandlerTest.php | 67 +++++++++ .../Messenger/EarlyExpirationMessageTest.php | 63 ++++++++ src/Symfony/Component/Cache/composer.json | 4 +- 18 files changed, 560 insertions(+), 30 deletions(-) create mode 100644 src/Symfony/Component/Cache/Messenger/EarlyExpirationDispatcher.php create mode 100644 src/Symfony/Component/Cache/Messenger/EarlyExpirationHandler.php create mode 100644 src/Symfony/Component/Cache/Messenger/EarlyExpirationMessage.php create mode 100644 src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php create mode 100644 src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php create mode 100644 src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationMessageTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index c1ce91dd4d..8cb4b2803e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -1041,6 +1041,9 @@ class Configuration implements ConfigurationInterface ->scalarNode('provider') ->info('Overwrite the setting from the default provider for this adapter.') ->end() + ->scalarNode('early_expiration_message_bus') + ->example('"messenger.default_bus" to send early expiration events to the default Messenger bus.') + ->end() ->scalarNode('clearer')->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 382b2fe640..e375b3c555 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -147,6 +147,7 @@ use Symfony\Component\Workflow\WorkflowInterface; use Symfony\Component\Yaml\Command\LintCommand as BaseYamlLintCommand; use Symfony\Component\Yaml\Yaml; use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\CallbackInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\Service\ResetInterface; @@ -436,6 +437,8 @@ class FrameworkExtension extends Extension ->addTag('container.env_var_loader'); $container->registerForAutoconfiguration(EnvVarProcessorInterface::class) ->addTag('container.env_var_processor'); + $container->registerForAutoconfiguration(CallbackInterface::class) + ->addTag('container.reversible'); $container->registerForAutoconfiguration(ServiceLocator::class) ->addTag('container.service_locator'); $container->registerForAutoconfiguration(ServiceSubscriberInterface::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php index 6f82bc6012..b44f3b9fb3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php @@ -25,6 +25,7 @@ use Symfony\Component\Cache\Adapter\RedisAdapter; use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter; use Symfony\Component\Cache\Adapter\TagAwareAdapter; use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Messenger\EarlyExpirationHandler; use Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer; use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; @@ -212,6 +213,12 @@ return static function (ContainerConfigurator $container) { null, // use igbinary_serialize() when available ]) + ->set('cache.early_expiration_handler', EarlyExpirationHandler::class) + ->args([ + service('reverse_container'), + ]) + ->tag('messenger.message_handler') + ->set('cache.default_clearer', Psr6CacheClearer::class) ->args([ [], diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 08cea8ecee..3f5c803baa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -284,6 +284,7 @@ + diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md index 2295089ad4..33889dbe77 100644 --- a/src/Symfony/Component/Cache/CHANGELOG.md +++ b/src/Symfony/Component/Cache/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.2.0 +----- + + * added integration with Messenger to allow computing cached values in a worker + 5.1.0 ----- diff --git a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php index fc78242b3a..05bee86412 100644 --- a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php +++ b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Cache\DependencyInjection; use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\ChainAdapter; +use Symfony\Component\Cache\Messenger\EarlyExpirationDispatcher; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -32,8 +33,11 @@ class CachePoolPass implements CompilerPassInterface private $cachePoolClearerTag; private $cacheSystemClearerId; private $cacheSystemClearerTag; + private $reverseContainerId; + private $reversibleTag; + private $messageHandlerId; - 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') + 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') { $this->cachePoolTag = $cachePoolTag; $this->kernelResetTag = $kernelResetTag; @@ -41,6 +45,9 @@ class CachePoolPass implements CompilerPassInterface $this->cachePoolClearerTag = $cachePoolClearerTag; $this->cacheSystemClearerId = $cacheSystemClearerId; $this->cacheSystemClearerTag = $cacheSystemClearerTag; + $this->reverseContainerId = $reverseContainerId; + $this->reversibleTag = $reversibleTag; + $this->messageHandlerId = $messageHandlerId; } /** @@ -55,6 +62,7 @@ class CachePoolPass implements CompilerPassInterface $seed .= '.'.$container->getParameter('kernel.container_class'); } + $needsMessageHandler = false; $allPools = []; $clearers = []; $attributes = [ @@ -62,6 +70,7 @@ class CachePoolPass implements CompilerPassInterface 'name', 'namespace', 'default_lifetime', + 'early_expiration_message_bus', 'reset', ]; foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $tags) { @@ -155,13 +164,24 @@ class CachePoolPass implements CompilerPassInterface if ($tags[0][$attr]) { $pool->addTag($this->kernelResetTag, ['method' => $tags[0][$attr]]); } + } elseif ('early_expiration_message_bus' === $attr) { + $needsMessageHandler = true; + $pool->addMethodCall('setCallbackWrapper', [(new Definition(EarlyExpirationDispatcher::class)) + ->addArgument(new Reference($tags[0]['early_expiration_message_bus'])) + ->addArgument(new Reference($this->reverseContainerId)) + ->addArgument((new Definition('callable')) + ->setFactory([new Reference($id), 'setCallbackWrapper']) + ->addArgument(null) + ), + ]); + $pool->addTag($this->reversibleTag); } elseif ('namespace' !== $attr || ArrayAdapter::class !== $class) { $pool->replaceArgument($i++, $tags[0][$attr]); } unset($tags[0][$attr]); } if (!empty($tags[0])) { - 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])))); + 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])))); } if (null !== $clearer) { @@ -171,6 +191,10 @@ class CachePoolPass implements CompilerPassInterface $allPools[$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); } + if (!$needsMessageHandler) { + $container->removeDefinition($this->messageHandlerId); + } + $notAliasedCacheClearerId = $this->cacheClearerId; while ($container->hasAlias($this->cacheClearerId)) { $this->cacheClearerId = (string) $container->getAlias($this->cacheClearerId); diff --git a/src/Symfony/Component/Cache/Messenger/EarlyExpirationDispatcher.php b/src/Symfony/Component/Cache/Messenger/EarlyExpirationDispatcher.php new file mode 100644 index 0000000000..6f11b8b5a2 --- /dev/null +++ b/src/Symfony/Component/Cache/Messenger/EarlyExpirationDispatcher.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Messenger; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Stamp\HandledStamp; + +/** + * Sends the computation of cached values to a message bus. + */ +class EarlyExpirationDispatcher +{ + private $bus; + private $reverseContainer; + private $callbackWrapper; + + public function __construct(MessageBusInterface $bus, ReverseContainer $reverseContainer, callable $callbackWrapper = null) + { + $this->bus = $bus; + $this->reverseContainer = $reverseContainer; + $this->callbackWrapper = $callbackWrapper; + } + + public function __invoke(callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, LoggerInterface $logger = null) + { + if (!$item->isHit() || null === $message = EarlyExpirationMessage::create($this->reverseContainer, $callback, $item, $pool)) { + // The item is stale or the callback cannot be reversed: we must compute the value now + $logger && $logger->info('Computing item "{key}" online: '.($item->isHit() ? 'callback cannot be reversed' : 'item is stale'), ['key' => $item->getKey()]); + + return null !== $this->callbackWrapper ? ($this->callbackWrapper)($callback, $item, $save, $pool, $setMetadata, $logger) : $callback($item, $save); + } + + $envelope = $this->bus->dispatch($message); + + if ($logger) { + if ($envelope->last(HandledStamp::class)) { + $logger->info('Item "{key}" was computed online', ['key' => $item->getKey()]); + } else { + $logger->info('Item "{key}" sent for recomputation', ['key' => $item->getKey()]); + } + } + + // The item's value is not stale, no need to write it to the backend + $save = false; + + return $message->getItem()->get() ?? $item->get(); + } +} diff --git a/src/Symfony/Component/Cache/Messenger/EarlyExpirationHandler.php b/src/Symfony/Component/Cache/Messenger/EarlyExpirationHandler.php new file mode 100644 index 0000000000..d7c4632e22 --- /dev/null +++ b/src/Symfony/Component/Cache/Messenger/EarlyExpirationHandler.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Messenger; + +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\Messenger\Handler\MessageHandlerInterface; + +/** + * Computes cached values sent to a message bus. + */ +class EarlyExpirationHandler implements MessageHandlerInterface +{ + private $reverseContainer; + private $processedNonces = []; + + public function __construct(ReverseContainer $reverseContainer) + { + $this->reverseContainer = $reverseContainer; + } + + public function __invoke(EarlyExpirationMessage $message) + { + $item = $message->getItem(); + $metadata = $item->getMetadata(); + $expiry = $metadata[CacheItem::METADATA_EXPIRY] ?? 0; + $ctime = $metadata[CacheItem::METADATA_CTIME] ?? 0; + + if ($expiry && $ctime) { + // skip duplicate or expired messages + + $processingNonce = [$expiry, $ctime]; + $pool = $message->getPool(); + $key = $item->getKey(); + + if (($this->processedNonces[$pool][$key] ?? null) === $processingNonce) { + return; + } + + if (microtime(true) >= $expiry) { + return; + } + + $this->processedNonces[$pool] = [$key => $processingNonce] + ($this->processedNonces[$pool] ?? []); + + if (\count($this->processedNonces[$pool]) > 100) { + array_pop($this->processedNonces[$pool]); + } + } + + static $setMetadata; + + $setMetadata = $setMetadata ?? \Closure::bind( + function (CacheItem $item, float $startTime) { + if ($item->expiry > $endTime = microtime(true)) { + $item->newMetadata[CacheItem::METADATA_EXPIRY] = $item->expiry; + $item->newMetadata[CacheItem::METADATA_CTIME] = (int) ceil(1000 * ($endTime - $startTime)); + } + }, + null, + CacheItem::class + ); + + $startTime = microtime(true); + $pool = $message->findPool($this->reverseContainer); + $callback = $message->findCallback($this->reverseContainer); + $value = $callback($item); + $setMetadata($item, $startTime); + $pool->save($item->set($value)); + } +} diff --git a/src/Symfony/Component/Cache/Messenger/EarlyExpirationMessage.php b/src/Symfony/Component/Cache/Messenger/EarlyExpirationMessage.php new file mode 100644 index 0000000000..e25c07e9a6 --- /dev/null +++ b/src/Symfony/Component/Cache/Messenger/EarlyExpirationMessage.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Messenger; + +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\DependencyInjection\ReverseContainer; + +/** + * Conveys a cached value that needs to be computed. + */ +final class EarlyExpirationMessage +{ + private $item; + private $pool; + private $callback; + + public static function create(ReverseContainer $reverseContainer, callable $callback, CacheItem $item, AdapterInterface $pool): ?self + { + try { + $item = clone $item; + $item->set(null); + } catch (\Exception $e) { + return null; + } + + $pool = $reverseContainer->getId($pool); + + if (\is_object($callback)) { + if (null === $id = $reverseContainer->getId($callback)) { + return null; + } + + $callback = '@'.$id; + } elseif (!\is_array($callback)) { + $callback = (string) $callback; + } elseif (!\is_object($callback[0])) { + $callback = [(string) $callback[0], (string) $callback[1]]; + } else { + if (null === $id = $reverseContainer->getId($callback[0])) { + return null; + } + + $callback = ['@'.$id, (string) $callback[1]]; + } + + return new self($item, $pool, $callback); + } + + public function getItem(): CacheItem + { + return $this->item; + } + + public function getPool(): string + { + return $this->pool; + } + + public function getCallback() + { + return $this->callback; + } + + public function findPool(ReverseContainer $reverseContainer): AdapterInterface + { + return $reverseContainer->getService($this->pool); + } + + public function findCallback(ReverseContainer $reverseContainer): callable + { + if (\is_string($callback = $this->callback)) { + return '@' === $callback[0] ? $reverseContainer->getService(substr($callback, 1)) : $callback; + } + if ('@' === $callback[0][0]) { + $callback[0] = $reverseContainer->getService(substr($callback[0], 1)); + } + + return $callback; + } + + private function __construct(CacheItem $item, string $pool, $callback) + { + $this->item = $item; + $this->pool = $pool; + $this->callback = $callback; + } +} diff --git a/src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php index 54264eeac5..74c6ee8704 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php @@ -13,6 +13,7 @@ namespace Symfony\Component\Cache\Tests\Adapter; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Filesystem\Filesystem; /** * @group time-sensitive @@ -26,29 +27,7 @@ class FilesystemAdapterTest extends AdapterTestCase public static function tearDownAfterClass(): void { - self::rmdir(sys_get_temp_dir().'/symfony-cache'); - } - - public static function rmdir(string $dir) - { - if (!file_exists($dir)) { - return; - } - if (!$dir || 0 !== strpos(\dirname($dir), sys_get_temp_dir())) { - throw new \Exception(__METHOD__."() operates only on subdirs of system's temp dir"); - } - $children = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS), - \RecursiveIteratorIterator::CHILD_FIRST - ); - foreach ($children as $child) { - if ($child->isDir()) { - rmdir($child); - } else { - unlink($child); - } - } - rmdir($dir); + (new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache'); } protected function isPruned(CacheItemPoolInterface $cache, string $name): bool diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php index 69f334656d..f1ee0d6c71 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterTest.php @@ -16,6 +16,7 @@ use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\Filesystem\Filesystem; /** * @group time-sensitive @@ -69,7 +70,7 @@ class PhpArrayAdapterTest extends AdapterTestCase $this->createCachePool()->clear(); if (file_exists(sys_get_temp_dir().'/symfony-cache')) { - FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); + (new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache'); } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php index a3e998b4b2..265b55e5ea 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpArrayAdapterWithFallbackTest.php @@ -14,6 +14,7 @@ namespace Symfony\Component\Cache\Tests\Adapter; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\Filesystem\Filesystem; /** * @group time-sensitive @@ -41,7 +42,7 @@ class PhpArrayAdapterWithFallbackTest extends AdapterTestCase $this->createCachePool()->clear(); if (file_exists(sys_get_temp_dir().'/symfony-cache')) { - FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); + (new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache'); } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PhpFilesAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PhpFilesAdapterTest.php index d204ef8d29..e084114e48 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PhpFilesAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PhpFilesAdapterTest.php @@ -13,6 +13,7 @@ namespace Symfony\Component\Cache\Tests\Adapter; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\PhpFilesAdapter; +use Symfony\Component\Filesystem\Filesystem; /** * @group time-sensitive @@ -30,7 +31,7 @@ class PhpFilesAdapterTest extends AdapterTestCase public static function tearDownAfterClass(): void { - FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); + (new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache'); } protected function isPruned(CacheItemPoolInterface $cache, string $name): bool diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php index 4d60f4cbd4..bb47794b86 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php @@ -20,6 +20,7 @@ use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\TagAwareAdapter; use Symfony\Component\Cache\Tests\Fixtures\PrunableAdapter; use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait; +use Symfony\Component\Filesystem\Filesystem; /** * @group time-sensitive @@ -35,7 +36,7 @@ class TagAwareAdapterTest extends AdapterTestCase public static function tearDownAfterClass(): void { - FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache'); + (new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache'); } /** diff --git a/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php new file mode 100644 index 0000000000..56c505f4b0 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Messenger; + +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Psr\Log\Test\TestLogger; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Messenger\EarlyExpirationDispatcher; +use Symfony\Component\Cache\Messenger\EarlyExpirationMessage; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\MessageBusInterface; + +/** + * @requires function Symfony\Component\DependencyInjection\ReverseContainer::__construct + */ +class EarlyExpirationDispatcherTest extends TestCase +{ + public static function tearDownAfterClass(): void + { + (new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache'); + } + + public function testFetch() + { + $logger = new TestLogger(); + $pool = new FilesystemAdapter(); + $pool->setLogger($logger); + + $item = $pool->getItem('foo'); + + $computationService = new class() { + public function __invoke(CacheItem $item) + { + return 123; + } + }; + + $container = new Container(); + $container->set('computation_service', $computationService); + $container->set('cache_pool', $pool); + + $reverseContainer = new ReverseContainer($container, new ServiceLocator([])); + + $bus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); + + $dispatcher = new EarlyExpirationDispatcher($bus, $reverseContainer); + + $saveResult = null; + $pool->setCallbackWrapper(function (callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger) use ($dispatcher, &$saveResult) { + try { + return $dispatcher($callback, $item, $save, $pool, $setMetadata, $logger); + } finally { + $saveResult = $save; + } + }); + + $this->assertSame(345, $pool->get('foo', function () { return 345; })); + $this->assertTrue($saveResult); + + $expected = [ + [ + 'level' => 'info', + 'message' => 'Computing item "{key}" online: item is stale', + 'context' => ['key' => 'foo'], + ], + ]; + $this->assertSame($expected, $logger->records); + } + + public function testEarlyExpiration() + { + $logger = new TestLogger(); + $pool = new FilesystemAdapter(); + $pool->setLogger($logger); + + $item = $pool->getItem('foo'); + $pool->save($item->set(789)); + $item = $pool->getItem('foo'); + + $computationService = new class() { + public function __invoke(CacheItem $item) + { + return 123; + } + }; + + $container = new Container(); + $container->set('computation_service', $computationService); + $container->set('cache_pool', $pool); + + $reverseContainer = new ReverseContainer($container, new ServiceLocator([])); + $msg = EarlyExpirationMessage::create($reverseContainer, $computationService, $item, $pool); + + $bus = $this->getMockBuilder(MessageBusInterface::class)->getMock(); + $bus->expects($this->once()) + ->method('dispatch') + ->with($msg) + ->willReturn(new Envelope($msg)); + + $dispatcher = new EarlyExpirationDispatcher($bus, $reverseContainer); + + $saveResult = true; + $setMetadata = function () { + }; + $dispatcher($computationService, $item, $saveResult, $pool, $setMetadata, $logger); + + $this->assertFalse($saveResult); + + $expected = [ + [ + 'level' => 'info', + 'message' => 'Item "{key}" sent for recomputation', + 'context' => ['key' => 'foo'], + ], + ]; + $this->assertSame($expected, $logger->records); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php new file mode 100644 index 0000000000..1953d2274e --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Messenger; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Messenger\EarlyExpirationHandler; +use Symfony\Component\Cache\Messenger\EarlyExpirationMessage; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\Filesystem\Filesystem; + +/** + * @requires function Symfony\Component\DependencyInjection\ReverseContainer::__construct + */ +class EarlyExpirationHandlerTest extends TestCase +{ + public static function tearDownAfterClass(): void + { + (new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache'); + } + + public function testHandle() + { + $pool = new FilesystemAdapter(); + $item = $pool->getItem('foo'); + $item->set(234); + + $computationService = new class() { + public function __invoke(CacheItem $item) + { + usleep(30000); + $item->expiresAfter(3600); + + return 123; + } + }; + + $container = new Container(); + $container->set('computation_service', $computationService); + $container->set('cache_pool', $pool); + + $reverseContainer = new ReverseContainer($container, new ServiceLocator([])); + + $msg = EarlyExpirationMessage::create($reverseContainer, $computationService, $item, $pool); + + $handler = new EarlyExpirationHandler($reverseContainer); + + $handler($msg); + + $this->assertSame(123, $pool->get('foo', [$this, 'fail'], 0.0, $metadata)); + + $this->assertGreaterThan(25, $metadata['ctime']); + $this->assertGreaterThan(time(), $metadata['expiry']); + } +} diff --git a/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationMessageTest.php b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationMessageTest.php new file mode 100644 index 0000000000..038357a499 --- /dev/null +++ b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationMessageTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Tests\Messenger; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Messenger\EarlyExpirationMessage; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\DependencyInjection\ServiceLocator; + +/** + * @requires function Symfony\Component\DependencyInjection\ReverseContainer::__construct + */ +class EarlyExpirationMessageTest extends TestCase +{ + public function testCreate() + { + $pool = new ArrayAdapter(); + $item = $pool->getItem('foo'); + $item->set(234); + + $computationService = new class() { + public function __invoke(CacheItem $item) + { + return 123; + } + }; + + $container = new Container(); + $container->set('computation_service', $computationService); + $container->set('cache_pool', $pool); + + $reverseContainer = new ReverseContainer($container, new ServiceLocator([])); + + $msg = EarlyExpirationMessage::create($reverseContainer, [$computationService, '__invoke'], $item, $pool); + + $this->assertSame('cache_pool', $msg->getPool()); + $this->assertSame($pool, $msg->findPool($reverseContainer)); + + $this->assertSame('foo', $msg->getItem()->getKey()); + $this->assertNull($msg->getItem()->get()); + $this->assertSame(234, $item->get()); + + $this->assertSame(['@computation_service', '__invoke'], $msg->getCallback()); + $this->assertSame([$computationService, '__invoke'], $msg->findCallback($reverseContainer)); + + $msg = EarlyExpirationMessage::create($reverseContainer, $computationService, $item, $pool); + + $this->assertSame('@computation_service', $msg->getCallback()); + $this->assertSame($computationService, $msg->findCallback($reverseContainer)); + } +} diff --git a/src/Symfony/Component/Cache/composer.json b/src/Symfony/Component/Cache/composer.json index 88c3cc4fd7..c1363a3e35 100644 --- a/src/Symfony/Component/Cache/composer.json +++ b/src/Symfony/Component/Cache/composer.json @@ -23,7 +23,7 @@ "require": { "php": ">=7.2.5", "psr/cache": "~1.0", - "psr/log": "~1.0", + "psr/log": "^1.1", "symfony/cache-contracts": "^1.1.7|^2", "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.1|^2", @@ -37,6 +37,8 @@ "psr/simple-cache": "^1.0", "symfony/config": "^4.4|^5.0", "symfony/dependency-injection": "^4.4|^5.0", + "symfony/filesystem": "^4.4|^5.0", + "symfony/messenger": "^4.4|^5.0", "symfony/var-dumper": "^4.4|^5.0" }, "conflict": {