From 2e19ac9eb4467428f1a27a7ffdec1a69c611d8b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20R?= Date: Mon, 19 Nov 2018 16:58:24 +0100 Subject: [PATCH 01/17] EZP-29821: [SPI Cache] Add cache for LanguageHandler->loadAll() --- .../Cache/ContentLanguageHandler.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/eZ/Publish/Core/Persistence/Cache/ContentLanguageHandler.php b/eZ/Publish/Core/Persistence/Cache/ContentLanguageHandler.php index 47557e0d22a..5f32ac2c2c1 100644 --- a/eZ/Publish/Core/Persistence/Cache/ContentLanguageHandler.php +++ b/eZ/Publish/Core/Persistence/Cache/ContentLanguageHandler.php @@ -23,6 +23,7 @@ class ContentLanguageHandler extends AbstractHandler implements ContentLanguageH public function create(CreateStruct $struct) { $this->logger->logCall(__METHOD__, array('struct' => $struct)); + $this->cache->deleteItem('ez-language-list'); return $this->persistenceHandler->contentLanguageHandler()->create($struct); } @@ -123,9 +124,24 @@ function (Language $language) { */ public function loadAll() { + $cacheItem = $this->cache->getItem('ez-language-list'); + if ($cacheItem->isHit()) { + return $cacheItem->get(); + } + $this->logger->logCall(__METHOD__); + $languages = $this->persistenceHandler->contentLanguageHandler()->loadAll(); + + $cacheTags = []; + foreach ($languages as $language) { + $cacheTags[] = 'language-' . $language->id; + } + + $cacheItem->set($languages); + $cacheItem->tag($cacheTags); + $this->cache->save($cacheItem); - return $this->persistenceHandler->contentLanguageHandler()->loadAll(); + return $languages; } /** From b4a72a0b1369434ad52622ef96f5a854e47166a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20R?= Date: Mon, 19 Nov 2018 16:58:55 +0100 Subject: [PATCH 02/17] EZP-29821: [SPI Cache] Add cache for ContentTypeHandler->loadAllGroups() --- .../Persistence/Cache/ContentTypeHandler.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/eZ/Publish/Core/Persistence/Cache/ContentTypeHandler.php b/eZ/Publish/Core/Persistence/Cache/ContentTypeHandler.php index edac444c014..865be6b9be1 100644 --- a/eZ/Publish/Core/Persistence/Cache/ContentTypeHandler.php +++ b/eZ/Publish/Core/Persistence/Cache/ContentTypeHandler.php @@ -31,6 +31,7 @@ class ContentTypeHandler extends AbstractHandler implements ContentTypeHandlerIn public function createGroup(GroupCreateStruct $struct) { $this->logger->logCall(__METHOD__, array('struct' => $struct)); + $this->cache->deleteItem('ez-content-type-group-list'); return $this->persistenceHandler->contentTypeHandler()->createGroup($struct); } @@ -125,9 +126,24 @@ public function loadGroupByIdentifier($identifier) */ public function loadAllGroups() { + $cacheItem = $this->cache->getItem('ez-content-type-group-list'); + if ($cacheItem->isHit()) { + return $cacheItem->get(); + } + $this->logger->logCall(__METHOD__); + $groups = $this->persistenceHandler->contentTypeHandler()->loadAllGroups(); + + $cacheTags = []; + foreach ($groups as $group) { + $cacheTags[] = 'type-group-' . $group->id; + } + + $cacheItem->set($groups); + $cacheItem->tag($cacheTags); + $this->cache->save($cacheItem); - return $this->persistenceHandler->contentTypeHandler()->loadAllGroups(); + return $groups; } /** From 73ed6f75c967cb13f3f1149ea1a06b8231a351f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20R?= Date: Thu, 21 Feb 2019 17:08:18 +0100 Subject: [PATCH 03/17] Change persistance logger to also log cache hits --- .../Collector/PersistenceCacheCollector.php | 44 +++- .../Resources/views/Profiler/layout.html.twig | 9 +- .../Profiler/persistence/panel.html.twig | 78 +++++-- .../Profiler/persistence/toolbar.html.twig | 13 +- .../Persistence/Cache/PersistenceLogger.php | 202 ++++++++++++++---- .../Cache/Tests/PersistenceLoggerTest.php | 23 +- 6 files changed, 287 insertions(+), 82 deletions(-) diff --git a/eZ/Bundle/EzPublishDebugBundle/Collector/PersistenceCacheCollector.php b/eZ/Bundle/EzPublishDebugBundle/Collector/PersistenceCacheCollector.php index b1fbaa28fc0..a761c4a7483 100644 --- a/eZ/Bundle/EzPublishDebugBundle/Collector/PersistenceCacheCollector.php +++ b/eZ/Bundle/EzPublishDebugBundle/Collector/PersistenceCacheCollector.php @@ -31,7 +31,7 @@ public function __construct(PersistenceLogger $logger) public function collect(Request $request, Response $response, \Exception $exception = null) { $this->data = [ - 'count' => $this->logger->getCount(), + 'stats' => $this->logger->getStats(), 'calls_logging_enabled' => $this->logger->isCallsLoggingEnabled(), 'calls' => $this->logger->getCalls(), 'handlers' => $this->logger->getLoadedUnCachedHandlers(), @@ -46,11 +46,25 @@ public function getName() /** * Returns call count. * + * @deprecaterd since 7.5, use getStats(). + * * @return int */ public function getCount() { - return $this->data['count']; + return $this->data['stats']['call'] + $this->data['stats']['miss']; + } + + /** + * Returns stats on Persistance cache usage. + * + * @since 7.5 + * + * @return int[] + */ + public function getStats() + { + return $this->data['stats']; } /** @@ -66,27 +80,35 @@ public function getCallsLoggingEnabled() } /** - * Returns calls. + * Returns all calls. * * @return array */ public function getCalls() { - $calls = []; - foreach ($this->data['calls'] as $call) { + if (empty($this->data['calls'])) { + return []; + } + + $calls = $count = []; + foreach ($this->data['calls'] as $hash => $call) { list($class, $method) = explode('::', $call['method']); $namespace = explode('\\', $class); $class = array_pop($namespace); - $calls[] = array( + $calls[$hash] = [ 'namespace' => $namespace, 'class' => $class, 'method' => $method, - 'arguments' => empty($call['arguments']) ? - '' : - preg_replace(array('/^array\s\(\s/', '/,\s\)$/'), '', var_export($call['arguments'], true)), - 'trace' => implode(', ', $call['trace']), - ); + 'arguments' => $call['arguments'], + 'traces' => $call['traces'], + 'stats' => $call['stats'], + ]; + // Leave out in-memory lookups from sorting + $count[$hash] = $call['stats']['uncached'] + $call['stats']['miss'] + $call['stats']['hit']; } + unset($data); + + array_multisort($count, SORT_DESC, $calls); return $calls; } diff --git a/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/layout.html.twig b/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/layout.html.twig index 9fcb32e2d5d..e301e627c10 100644 --- a/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/layout.html.twig +++ b/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/layout.html.twig @@ -1,8 +1,6 @@ {% extends 'WebProfilerBundle:Profiler:layout.html.twig' %} {% block toolbar %} - {# {% set status = (collector.hits/collector.calls) < 0.33 ? 'red' : (collector.hits/collector.calls) < 0.66 ? 'yellow' : '' %} #} - {% set icon %} @@ -25,7 +23,12 @@ {% endset %} - {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status|default('') }) }} + {# Set to red if over 100 uncached, and to yellow if either over 15 uncached or over 100 cache hits lookups #} + {% set stats = collector.getCollector('ezpublish.debug.persistence').stats %} + {% set total_uncached = stats.uncached + stats.miss %} + {% set status_logo = total_uncached > 100 ? 'red' : (total_uncached > 15 ? 'yellow' : (stats.hit > 100 ? 'yellow' : '')) %} + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_logo|default('') }) }} {% endblock %} {% block menu %} diff --git a/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/persistence/panel.html.twig b/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/persistence/panel.html.twig index 8841524f516..ed46225c9ce 100644 --- a/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/persistence/panel.html.twig +++ b/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/persistence/panel.html.twig @@ -1,37 +1,89 @@ -

- Uncached Persistence\Cache calls +

+ Persistence\Cache calls

-

TIP: This can represent both cold cache, and calls to methods which has not been made to be cached (yet). Make sure to reload a few times to warmup cache in order to only see the latter.

+{% set stats = collector.stats %} - - + + + + + + {% if collector.handlerscount %} - + {% endif %}
Total Uncached calls:{{ collector.count }}Uncached method calls:{{ stats.uncached }}
Cached method calls:From memory: {{ stats.memory }}, hits: {{ stats.hit }}, misses: {{ stats.miss }}
Uncached SPI handlers(times loaded):Uncached SPI handlers: {{ collector.handlers|join(', ') }}
{% if collector.callsLoggingEnabled %} + +

+ Logged calls to Persistence\Cache +

+ +

+ TIP: Calls are ordered by # of backend lookups. As misses can represent cold cache, make sure to reload corresponding page to warmup cache a few times to see info on cached paged. +

+ - - + - + + {% for call in collector.calls %} - - - - + + + {% endfor %}
ClassMethodClass::method ArgumentsTraceTraces
{{ call.class }}{{ call.method }}{{ call.arguments }}{{ call.trace }} + {{ call.class }}::{{ call.method }}
+

+ {% if call.stats.uncached %}
Uncached calls: {{ call.stats.uncached }}{% endif %} + {% if call.stats.miss %}
Cache misses: {{ call.stats.miss }}{% endif %} + {% if call.stats.hit %}
Cache hits: {{ call.stats.hit }}{% endif %} + {% if call.stats.memory %}
Cache hits from memory: {{ call.stats.memory }}{% endif %} +

+
+ {% for key, argument in call.arguments %} + {{ key }}: + {% if argument is iterable %} + {{ argument|join(', ') }} + {% else %} + {{ argument }} + {% endif %} +
+ {% endfor %} +
+ + + + + + {% for traceInfo in call.traces %} + + + + + {% endfor %} +
#Trace
+ {{ traceInfo.count }} + + {% for calltrace in traceInfo.trace %} + {{ calltrace }}
+ {% endfor %} +
+
+ +{% else %} +

TIP: Call logging is disabled by default as it consumes considerably memory, enable @TODO in order to see calls made and trace for where the calls comes from.

{% endif %} diff --git a/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/persistence/toolbar.html.twig b/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/persistence/toolbar.html.twig index 3ad4b8670ff..d9c196ffba6 100644 --- a/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/persistence/toolbar.html.twig +++ b/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/persistence/toolbar.html.twig @@ -1,9 +1,16 @@ +{% set stats = collector.stats %} +{# Set cache status to green if misses is below 1 and total hits is below 20 (with remote Redis 20 cache lookups can take 50-100ms) #} +{% set status_cache = stats.miss > 100 ? 'red' : (stats.hit > 100 ? 'yellow' : (stats.miss < 1 and stats.hit < 20 ? 'green' : '')) %} +
- SPI (persistence) + SPI Persistence/Cache
- calls {{ collector.count }} + uncached calls {{ stats.uncached }}
- handlers {{ collector.handlerscount }} + misses / hits / memory {{ stats.miss }} / {{ stats.hit }} / {{ stats.memory }} +
+
+ uncached handlers {{ collector.handlerscount }}
diff --git a/eZ/Publish/Core/Persistence/Cache/PersistenceLogger.php b/eZ/Publish/Core/Persistence/Cache/PersistenceLogger.php index 84b819132c4..c4c5362246e 100644 --- a/eZ/Publish/Core/Persistence/Cache/PersistenceLogger.php +++ b/eZ/Publish/Core/Persistence/Cache/PersistenceLogger.php @@ -6,21 +6,26 @@ * @copyright Copyright (C) eZ Systems AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); + namespace eZ\Publish\Core\Persistence\Cache; /** - * Log un-cached use of SPI Persistence. - * - * Stops logging details when reaching $maxLogCalls to conserve memory use + * Log un-cached & cached use of SPI Persistence. */ class PersistenceLogger { const NAME = 'PersistenceLogger'; /** - * @var int + * @var int[] */ - protected $count = 0; + protected $stats = [ + 'uncached' => 0, + 'miss' => 0, + 'hit' => 0, + 'memory' => 0, + ]; /** * @var bool @@ -30,57 +35,168 @@ class PersistenceLogger /** * @var array */ - protected $calls = array(); + protected $calls = []; /** * @var array */ - protected $unCachedHandlers = array(); + protected $unCachedHandlers = []; /** - * @param bool $logCalls Flag to enable logging of calls or not, should be disabled in prod + * @param bool $logCalls Flag to enable logging of calls or not, provides extra debug info about calls made to SPI + * level, including where they come form. However this uses quite a bit of memory. */ - public function __construct($logCalls = true) + public function __construct(bool $logCalls = true) { $this->logCalls = $logCalls; } /** - * Log SPI calls with method name and arguments until $maxLogCalls is reached. + * Log uncached SPI calls with method name and arguments. + * + * NOTE: As of 7.5 this method is meant for logging calls to uncached spi method calls, + * for cache miss calls to cached SPI methods migrate to use {@see logCacheMiss()}. * * @param string $method * @param array $arguments */ - public function logCall($method, array $arguments = array()) + public function logCall(string $method, array $arguments = []): void + { + ++$this->stats['uncached']; + if (!$this->logCalls) { + return; + } + + $this->collectCacheCallData( + $method, + $arguments, + \array_slice( + \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 7), + 2 + ), + 'uncached' + ); + } + + /** + * Log Cache miss, gets info it needs by backtrace if needed. + * + * @since 7.5 + * + * @param array $arguments + * @param int $traceOffset + */ + public function logCacheMiss(array $arguments = [], int $traceOffset = 2): void + { + ++$this->stats['miss']; + if (!$this->logCalls) { + return; + } + + $trace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 7 + $traceOffset); + $this->collectCacheCallData( + $trace[$traceOffset - 1]['class'] . '::' . $trace[$traceOffset - 1]['function'], + $arguments, + \array_slice($trace, $traceOffset), + 'miss' + ); + } + + /** + * Log a Cache hit, gets info it needs by backtrace if needed. + * + * @since 7.5 + * + * @param array $arguments + * @param int $traceOffset + * @param bool $inMemory Denotes is cache hit was from memory (php variable), as opposed to from cache pool which + * is usually disk or remote cache service. + */ + public function logCacheHit(array $arguments = [], int $traceOffset = 2, bool $inMemory = false): void + { + if ($inMemory) { + ++$this->stats['memory']; + } else { + ++$this->stats['hit']; + } + + if (!$this->logCalls) { + return; + } + + $trace = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 7 + $traceOffset); + $this->collectCacheCallData( + $trace[$traceOffset - 1]['class'] . '::' . $trace[$traceOffset - 1]['function'], + $arguments, + \array_slice($trace, $traceOffset), + $inMemory ? 'memory' : 'hit' + ); + } + + /** + * Collection method for {@see logCacheHit()}, {@see logCacheMiss()} & {@see logCall()}. + * + * @param $method + * @param array $arguments + * @param array $trimmedBacktrace + * @param string $type + */ + private function collectCacheCallData($method, array $arguments, array $trimmedBacktrace, string $type): void { - ++$this->count; - if ($this->logCalls) { - $this->calls[] = array( + // simplest/fastests hash possible to identify if we have already collected this before to save on memory use + $callHash = \hash('adler32', $method . \serialize($arguments)); + if (empty($this->calls[$callHash])) { + $this->calls[$callHash] = [ 'method' => $method, 'arguments' => $arguments, - 'trace' => $this->getSimpleCallTrace( - array_slice( - debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 7), - 2 - ) - ), - ); + 'stats' => [ + 'uncached' => 0, + 'miss' => 0, + 'hit' => 0, + 'memory' => 0, + ], + 'traces' => [], + ]; + } + ++$this->calls[$callHash]['stats'][$type]; + + // @todo If we want/need to save more memory we can consider making an value object holding "trace", and share + // the object across all calls having same trace (quite often the case). + $trace = $this->getSimpleCallTrace($trimmedBacktrace); + $traceHash = \hash('adler32', \implode($trace)); + if (empty($this->calls[$callHash]['traces'][$traceHash])) { + $this->calls[$callHash]['traces'][$traceHash] = [ + 'trace' => $trace, + 'count' => 0, + ]; } + ++$this->calls[$callHash]['traces'][$traceHash]['count']; } + /** + * Simplify trace to an array of strings. + * + * Skipps any traces from Syfony proxies or closures to make trace as readable as possible in as few lines as + * possible. And point is to identify which code outside kernel is triggering the SPI call, so trace stops one + * call after namespace is no longer in eZ\Publish\Core\. + * + * @param array $backtrace Partial backtrace from |debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) or similar. + * + * @return string[] + */ private function getSimpleCallTrace(array $backtrace): array { $calls = []; foreach ($backtrace as $call) { - if (!isset($call['class']) || strpos($call['class'], '\\') === false) { - // skip if class has no namspace (Symfony lazy proxy) or plain function + if (!isset($call['class'][2]) || ($call['class'][2] !== '\\' && \strpos($call['class'], '\\') === false)) { + // skip if class has no namespace (Symfony lazy proxy or plain function) continue; } $calls[] = $call['class'] . $call['type'] . $call['function'] . '()'; // Break out as soon as we have listed 1 class outside of kernel - if (strpos($call['class'], 'eZ\\Publish\\Core\\') !== 0) { + if ($call['class'][0] !== 'e' && \strpos($call['class'], 'eZ\\Publish\\Core\\') !== 0) { break; } } @@ -89,11 +205,11 @@ private function getSimpleCallTrace(array $backtrace): array } /** - * Log uncached handler being loaded. + * Log un-cached handler being loaded. * * @param string $handler */ - public function logUnCachedHandler($handler) + public function logUnCachedHandler(string $handler): void { if (!isset($this->unCachedHandlers[$handler])) { $this->unCachedHandlers[$handler] = 0; @@ -101,42 +217,42 @@ public function logUnCachedHandler($handler) ++$this->unCachedHandlers[$handler]; } - /** - * @return string - */ - public function getName() + public function getName(): string { return self::NAME; } /** - * @return int + * Counts the total of spi uncached call (cache miss and uncached calls). + * + * @deprecated Since 7.5, use getStats(). */ - public function getCount() + public function getCount(): int { - return $this->count; + return $this->stats['uncached'] + $this->stats['miss']; } /** - * @return bool + * Get stats (call/miss/hit/memory). + * + * @since 7.5 */ - public function isCallsLoggingEnabled() + public function getStats(): array + { + return $this->stats; + } + + public function isCallsLoggingEnabled(): bool { return $this->logCalls; } - /** - * @return array - */ - public function getCalls() + public function getCalls(): array { return $this->calls; } - /** - * @return array - */ - public function getLoadedUnCachedHandlers() + public function getLoadedUnCachedHandlers(): array { return $this->unCachedHandlers; } diff --git a/eZ/Publish/Core/Persistence/Cache/Tests/PersistenceLoggerTest.php b/eZ/Publish/Core/Persistence/Cache/Tests/PersistenceLoggerTest.php index 34b18edb2ef..40c4c783f7f 100644 --- a/eZ/Publish/Core/Persistence/Cache/Tests/PersistenceLoggerTest.php +++ b/eZ/Publish/Core/Persistence/Cache/Tests/PersistenceLoggerTest.php @@ -97,15 +97,20 @@ public function testGetCountValues($logger) */ public function testGetCallValues($logger) { + $calls = $logger->getCalls(); + // As we don't care about the hash index we get the array values instead + $calls = array_values($calls); + $method = __CLASS__ . '::testLogCall'; - $this->assertEquals( - [ - ['method' => $method, 'arguments' => [], 'trace' => ['PHPUnit\Framework\TestCase->runTest()']], - ['method' => $method, 'arguments' => [], 'trace' => ['PHPUnit\Framework\TestCase->runTest()']], - ['method' => $method, 'arguments' => [], 'trace' => ['PHPUnit\Framework\TestCase->runTest()']], - ['method' => $method, 'arguments' => [33], 'trace' => ['PHPUnit\Framework\TestCase->runTest()']], - ], - $logger->getCalls() - ); + + $this->assertEquals($method, $calls[0]['method']); + $this->assertEquals([], $calls[0]['arguments']); + $this->assertCount(1, $calls[0]['traces']); + $this->assertEquals(['uncached' => 3, 'miss' => 0, 'hit' => 0, 'memory' => 0], $calls[0]['stats']); + + $this->assertEquals($method, $calls[1]['method']); + $this->assertEquals([33], $calls[1]['arguments']); + $this->assertCount(1, $calls[1]['traces']); + $this->assertEquals(['uncached' => 1, 'miss' => 0, 'hit' => 0, 'memory' => 0], $calls[1]['stats']); } } From 6d6004e1cfe91ea1d1a42feb5fa216ae9b61a13e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20R?= Date: Fri, 4 Jan 2019 14:35:31 +0100 Subject: [PATCH 04/17] Add in-memory cache pool and abstract handler --- .../Cache/AbstractInMemoryHandler.php | 318 ++++++++++++++++++ .../Cache/InMemory/InMemoryCache.php | 192 +++++++++++ .../Core/Persistence/Cache/InMemory/README.md | 6 + .../Cache/Tests/AbstractBaseHandlerTest.php | 12 +- .../AbstractInMemoryCacheHandlerTest.php | 195 +++++++++++ .../Tests/InMemory/InMemoryCacheTest.php | 183 ++++++++++ .../Core/settings/storage_engines/cache.yml | 26 +- 7 files changed, 928 insertions(+), 4 deletions(-) create mode 100644 eZ/Publish/Core/Persistence/Cache/AbstractInMemoryHandler.php create mode 100644 eZ/Publish/Core/Persistence/Cache/InMemory/InMemoryCache.php create mode 100644 eZ/Publish/Core/Persistence/Cache/InMemory/README.md create mode 100644 eZ/Publish/Core/Persistence/Cache/Tests/AbstractInMemoryCacheHandlerTest.php create mode 100644 eZ/Publish/Core/Persistence/Cache/Tests/InMemory/InMemoryCacheTest.php diff --git a/eZ/Publish/Core/Persistence/Cache/AbstractInMemoryHandler.php b/eZ/Publish/Core/Persistence/Cache/AbstractInMemoryHandler.php new file mode 100644 index 00000000000..2eb424d9fc7 --- /dev/null +++ b/eZ/Publish/Core/Persistence/Cache/AbstractInMemoryHandler.php @@ -0,0 +1,318 @@ +cache = $cache; + $this->persistenceHandler = $persistenceHandler; + $this->logger = $logger; + $this->inMemory = $inMemory; + } + + /** + * Load one cache item from cache and loggs the hits / misses. + * + * Load items from in-memory cache, symfony cache pool or backend in that order. + * If not cached the returned objects will be placed in cache. + * + * @param int|string $id + * @param string $keyPrefix E.g "ez-content-" + * @param callable $backendLoader Function for loading missing objects, gets array with missing id's as argument, + * expects return value to be array with id as key. Missing items should be missing. + * @param callable $cacheTagger Gets cache object as argument, return array of cache tags. + * @param callable $cacheIndexes Gets cache object as argument, return array of cache keys. + * + * @return object + */ + final protected function getCacheValue( + $id, + string $keyPrefix, + callable $backendLoader, + callable $cacheTagger, + callable $cacheIndexes, + string $keyPostfix = '' + ) { + $key = $keyPrefix . $id . $keyPostfix; + // In-memory + if ($object = $this->inMemory->get($key)) { + $this->logger->logCacheHit([$id], 3, true); + + return $object; + } + + // Cache pool + $cacheItem = $this->cache->getItem($key); + if ($cacheItem->isHit()) { + $this->logger->logCacheHit([$id], 3); + $this->inMemory->setMulti([$object = $cacheItem->get()], $cacheIndexes); + + return $object; + } + + // Backend + $object = $backendLoader($id); + $this->inMemory->setMulti([$object], $cacheIndexes); + $this->logger->logCacheMiss([$id], 3); + $this->cache->save( + $cacheItem + ->set($object) + ->tag($cacheTagger($object)) + ); + + return $object; + } + + /** + * Load list of objects of some type and loggs the hits / misses. + * + * Load items from in-memory cache, symfony cache pool or backend in that order. + * If not cached the returned objects will be placed in cache. + * + * @param string $key + * @param callable $backendLoader Function for loading ALL objects, value is cached as-is. + * @param callable $cacheTagger Gets cache object as argument, return array of cache tags. + * @param callable $cacheIndexes Gets cache object as argument, return array of cache keys. + * @param array|callable $listTags Optional, global tags for the list cache, either as array or lazy callable. + * @param array $arguments Optional, arguments when parnt method takes arguments that key varies on. + * + * @return object + */ + final protected function getListCacheValue( + string $key, + callable $backendLoader, + callable $cacheTagger, + callable $cacheIndexes, + $listTags = [], + array $arguments = [] + ) { + // In-memory + if ($objects = $this->inMemory->get($key)) { + $this->logger->logCacheHit($arguments, 3, true); + + return $objects; + } + + // Cache pool + $cacheItem = $this->cache->getItem($key); + if ($cacheItem->isHit()) { + $this->logger->logCacheHit($arguments, 3); + $this->inMemory->setMulti($objects = $cacheItem->get(), $cacheIndexes, $key); + + return $objects; + } + + // Backend + $objects = $backendLoader(); + $this->inMemory->setMulti($objects, $cacheIndexes, $key); + $this->logger->logCacheMiss($arguments, 3); + + if (is_callable($listTags)) { + $tagSet = [$listTags()]; + } else { + $tagSet = [$listTags]; + } + + foreach ($objects as $object) { + $tagSet[] = $cacheTagger($object); + } + + $this->cache->save( + $cacheItem + ->set($objects) + ->tag(array_unique(array_merge(...$tagSet))) + ); + + return $objects; + } + + /** + * Load several cache items from cache and loggs the hits / misses. + * + * Load items from in-memory cache, symfony cache pool or backend in that order. + * If not cached the returned objects will be placed in cache. + * + * Cache items must be stored with a key in the following format "${keyPrefix}${id}", like "ez-content-info-${id}", + * in order for this method to be able to prefix key on id's and also extract key prefix afterwards. + * + * @param array $ids + * @param string $keyPrefix E.g "ez-content-" + * @param callable $backendLoader Function for loading missing objects, gets array with missing id's as argument, + * expects return value to be array with id as key. Missing items should be missing. + * @param callable $cacheTagger Gets cache object as argument, return array of cache tags. + * @param callable $cacheIndexes Gets cache object as argument, return array of cache keys. + * + * @return array + */ + final protected function getMultipleCacheValues( + array $ids, + string $keyPrefix, + callable $backendLoader, + callable $cacheTagger, + callable $cacheIndexes, + string $keyPostfix = '' + ): array { + if (empty($ids)) { + return []; + } + + // Generate unique cache keys and check if in-memory + $list = []; + $cacheKeys = []; + $cacheKeysToIdMap = []; + foreach (array_unique($ids) as $id) { + $key = $keyPrefix . $id . $keyPostfix; + if ($object = $this->inMemory->get($key)) { + $list[$id] = $object; + } else { + $cacheKeys[] = $key; + $cacheKeysToIdMap[$key] = $id; + } + } + + // No in-memory misses + if (empty($cacheKeys)) { + $this->logger->logCacheHit($ids, 3, true); + + return $list; + } + + // Load cache items by cache keys (will contain hits and misses) + $loaded = []; + $cacheMisses = []; + foreach ($this->cache->getItems($cacheKeys) as $key => $cacheItem) { + $id = $cacheKeysToIdMap[$key]; + if ($cacheItem->isHit()) { + $list[$id] = $cacheItem->get(); + $loaded[$id] = $list[$id]; + } else { + $cacheMisses[] = $id; + $list[$id] = $cacheItem; + } + } + + // No cache pool misses, cache loaded items in-memory and return + if (empty($cacheMisses)) { + $this->logger->logCacheHit($ids, 3, true); + $this->inMemory->setMulti($loaded, $cacheIndexes); + + return $list; + } + + // Load missing items, save to cache & apply to list if found + $backendLoadedList = $backendLoader($cacheMisses); + foreach ($cacheMisses as $id) { + if (isset($backendLoadedList[$id])) { + $this->cache->save( + $list[$id] + ->set($backendLoadedList[$id]) + ->tag($cacheTagger($backendLoadedList[$id])) + ); + $loaded[$id] = $backendLoadedList[$id]; + $list[$id] = $backendLoadedList[$id]; + } else { + // not found + unset($list[$id]); + } + } + + $this->inMemory->setMulti($loaded, $cacheIndexes); + unset($loaded, $backendLoadedList); + $this->logger->logCacheMiss($cacheMisses, 3); + + return $list; + } + + /** + * Delete cache by keys. + * + * @param array $keys + */ + final protected function deleteCache(array $keys): void + { + if (empty($keys)) { + return; + } + + $this->inMemory->deleteMulti($keys); + $this->cache->deleteItems($keys); + } + + /** + * Invalidate cache by tags. + * + * @param array $tags + */ + final protected function invalidateCache(array $tags): void + { + if (empty($tags)) { + return; + } + + $this->inMemory->clear(); + $this->cache->invalidateTags($tags); + } + + /** + * Clear ALL cache. + * + * Only use this in extname situations, or for isolation in tests. + */ + final protected function clearCache(): void + { + $this->inMemory->clear(); + $this->cache->clear(); + } +} diff --git a/eZ/Publish/Core/Persistence/Cache/InMemory/InMemoryCache.php b/eZ/Publish/Core/Persistence/Cache/InMemory/InMemoryCache.php new file mode 100644 index 00000000000..e4b36257cf8 --- /dev/null +++ b/eZ/Publish/Core/Persistence/Cache/InMemory/InMemoryCache.php @@ -0,0 +1,192 @@ +ttl = $ttl / 1000; + $this->limit = $limit; + $this->enabled = $enabled; + } + + /** + * @param bool $enabled + * + * @return bool Prior value + */ + public function setEnabled(bool $enabled = true): bool + { + $was = $this->enabled; + $this->enabled = $enabled; + + return $was; + } + + /** + * Returns a cache objects. + * + * @param string $key Primary or secondary index to look for cache on. + * + * @return object|null Object if found, null if not. + */ + public function get(string $key) + { + if ($this->enabled === false) { + return null; + } + + $index = $this->cacheIndex[$key] ?? $key; + if (!isset($this->cache[$index]) || $this->cacheTime[$index] + $this->ttl < microtime(true)) { + return null; + } + + return $this->cache[$index]; + } + + /** + * Set object in in-memory cache. + * + * Should only set Cache hits here! + * + * @param object[] $objects + * @param callable $objectIndexes Return array of indexes per object (first argument), must return at least 1 primary index + * @param string|null $listIndex Optional index for list of items + */ + public function setMulti(array $objects, callable $objectIndexes, string $listIndex = null): void + { + // If objects accounts for more then 20% of our limit, assume it's bulk content load and skip saving in-memory + if ($this->enabled === false || \count($objects) >= $this->limit / 5) { + return; + } + + // check if we will reach limit by adding these objects, if so remove old cache + if (\count($this->cache) + \count($objects) >= $this->limit) { + $this->vacuum(); + } + + $time = microtime(true); + // if set add objects to cache on list index (typically a "all" key) + if ($listIndex) { + $this->cache[$listIndex] = $objects; + $this->cacheTime[$listIndex] = $time; + } + + foreach ($objects as $object) { + // Skip if there are no indexes + if (!$indexes = $objectIndexes($object)) { + continue; + } + + $key = array_shift($indexes); + $this->cache[$key] = $object; + $this->cacheTime[$key] = $time; + + foreach ($indexes as $index) { + $this->cacheIndex[$index] = $key; + } + } + } + + /** + * Removes multiple in-memory cache from the pool. + * + * @param string[] $keys An array of keys that should be removed from the pool. + */ + public function deleteMulti(array $keys): void + { + if ($this->enabled === false) { + return; + } + + foreach ($keys as $key) { + if ($index = $this->cacheIndex[$key] ?? null) { + unset($this->cacheIndex[$key], $this->cache[$index], $this->cacheTime[$index]); + } else { + unset($this->cache[$key], $this->cacheTime[$key]); + } + } + } + + /** + * Deletes all cache in the in-memory pool. + */ + public function clear(): void + { + // On purpose does not check if enabled, in case of several instances we allow clearing cache + $this->cache = $this->cacheIndex = $this->cacheTime = []; + } + + /** + * Call to reduce cache items when $limit has been reached. + * + * Deletes expired first, then oldest(or least used?). + */ + private function vacuum(): void + { + // Vacuuming cache in bulk, clearing the 33% oldest cache values + $this->cache = \array_slice($this->cache, (int) ($this->limit / 3)); + + // Cleanup secondary index and cache time for missing primary keys + foreach ($this->cacheIndex as $index => $key) { + if (!isset($this->cache[$key])) { + unset($this->cacheIndex[$index], $this->cacheTime[$key]); + } + } + } +} diff --git a/eZ/Publish/Core/Persistence/Cache/InMemory/README.md b/eZ/Publish/Core/Persistence/Cache/InMemory/README.md new file mode 100644 index 00000000000..2782835fab2 --- /dev/null +++ b/eZ/Publish/Core/Persistence/Cache/InMemory/README.md @@ -0,0 +1,6 @@ +# In Memory cache classes + + +Classes for specific in-memory usage, at this level only in-frequently updated metadata is meant to be cached like this. +Examples: Languages, ContentTypes, ObjectStates, Sections + diff --git a/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php b/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php index 657ba68b5ca..936ba2d3a78 100644 --- a/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php +++ b/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php @@ -23,8 +23,9 @@ use eZ\Publish\Core\Persistence\Cache\BookmarkHandler as CacheBookmarkHandler; use eZ\Publish\Core\Persistence\Cache\NotificationHandler as CacheNotificationHandler; use eZ\Publish\Core\Persistence\Cache\UserPreferenceHandler as CacheUserPreferenceHandler; -use eZ\Publish\SPI\Persistence\Handler; +use eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache; use eZ\Publish\Core\Persistence\Cache\PersistenceLogger; +use eZ\Publish\SPI\Persistence\Handler; use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface; use Symfony\Component\Cache\CacheItem; use PHPUnit\Framework\TestCase; @@ -54,6 +55,11 @@ abstract class AbstractBaseHandlerTest extends TestCase */ protected $loggerMock; + /** + * @var \eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache|\PHPUnit\Framework\MockObject\MockObject + */ + protected $inMemoryMock; + /** * @var \Closure */ @@ -69,6 +75,7 @@ final protected function setUp() $this->persistenceHandlerMock = $this->createMock(Handler::class); $this->cacheMock = $this->createMock(TagAwareAdapterInterface::class); $this->loggerMock = $this->createMock(PersistenceLogger::class); + $this->inMemoryMock = $this->createMock(InMemoryCache::class); $this->persistenceCacheHandler = new CacheHandler( $this->persistenceHandlerMock, @@ -90,7 +97,7 @@ final protected function setUp() ); $this->cacheItemsClosure = \Closure::bind( - function ($key, $value, $isHit, $defaultLifetime = 0) { + static function ($key, $value, $isHit, $defaultLifetime = 0) { $item = new CacheItem(); $item->key = $key; $item->value = $value; @@ -114,6 +121,7 @@ final protected function tearDown() unset($this->persistenceCacheHandler); unset($this->loggerMock); unset($this->cacheItemsClosure); + unset($this->inMemoryMock); parent::tearDown(); } diff --git a/eZ/Publish/Core/Persistence/Cache/Tests/AbstractInMemoryCacheHandlerTest.php b/eZ/Publish/Core/Persistence/Cache/Tests/AbstractInMemoryCacheHandlerTest.php new file mode 100644 index 00000000000..1e608899a73 --- /dev/null +++ b/eZ/Publish/Core/Persistence/Cache/Tests/AbstractInMemoryCacheHandlerTest.php @@ -0,0 +1,195 @@ +getHandlerMethodName(); + + $this->loggerMock->expects($this->once())->method('logCall'); + $this->loggerMock->expects($this->never())->method('logCacheHit'); + $this->loggerMock->expects($this->never())->method('logCacheMiss'); + + $innerHandler = $this->createMock($this->getHandlerClassName()); + $this->persistenceHandlerMock + ->expects($this->once()) + ->method($handlerMethodName) + ->will($this->returnValue($innerHandler)); + + $innerHandler + ->expects($this->once()) + ->method($method) + ->with(...$arguments) + ->will($this->returnValue($returnValue)); + + if ($tags || $key) { + $this->cacheMock + ->expects(!empty($tags) ? $this->once() : $this->never()) + ->method('invalidateTags') + ->with($tags); + + $this->cacheMock + ->expects(!empty($key) ? $this->once() : $this->never()) + ->method('deleteItems') + ->with($key); + } else { + $this->cacheMock + ->expects($this->never()) + ->method($this->anything()); + } + + $handler = $this->persistenceCacheHandler->$handlerMethodName(); + call_user_func_array(array($handler, $method), $arguments); + } + + abstract public function providerForCachedLoadMethods(): array; + + /** + * @dataProvider providerForCachedLoadMethods + * + * @param string $method + * @param array $arguments + * @param string $key + * @param mixed $data + * @param bool $multi Default false, set to true if method will lookup several cache items. + * @param array $additionalCalls Sets of additional calls being made to handlers, with 4 values (0: handler name, 1: handler class, 2: method, 3: return data) + */ + final public function testLoadMethodsCacheHit(string $method, array $arguments, string $key, $data = null, bool $multi = false, array $additionalCalls = []) + { + $cacheItem = $this->getCacheItem($key, $multi ? reset($data) : $data); + $handlerMethodName = $this->getHandlerMethodName(); + + $this->loggerMock->expects($this->once())->method('logCacheHit'); + $this->loggerMock->expects($this->never())->method('logCall'); + $this->loggerMock->expects($this->never())->method('logCacheMiss'); + + if ($multi) { + $this->cacheMock + ->expects($this->once()) + ->method('getItems') + ->with([$cacheItem->getKey()]) + ->willReturn([$key => $cacheItem]); + } else { + $this->cacheMock + ->expects($this->once()) + ->method('getItem') + ->with($cacheItem->getKey()) + ->willReturn($cacheItem); + } + + $this->persistenceHandlerMock + ->expects($this->never()) + ->method($handlerMethodName); + + foreach ($additionalCalls as $additionalCall) { + $this->persistenceHandlerMock + ->expects($this->never()) + ->method($additionalCall[0]); + } + + $handler = $this->persistenceCacheHandler->$handlerMethodName(); + $return = call_user_func_array([$handler, $method], $arguments); + + $this->assertEquals($data, $return); + } + + /** + * @dataProvider providerForCachedLoadMethods + * + * @param string $method + * @param array $arguments + * @param string $key + * @param object $data + * @param bool $multi Default false, set to true if method will lookup several cache items. + * @param array $additionalCalls Sets of additional calls being made to handlers, with 4 values (0: handler name, 1: handler class, 2: method, 3: return data) + */ + final public function testLoadMethodsCacheMiss(string $method, array $arguments, string $key, $data = null, bool $multi = false, array $additionalCalls = []) + { + $cacheItem = $this->getCacheItem($key, null); + $handlerMethodName = $this->getHandlerMethodName(); + + $this->loggerMock->expects($this->once())->method('logCacheMiss'); + $this->loggerMock->expects($this->never())->method('logCall'); + $this->loggerMock->expects($this->never())->method('logCacheHit'); + + if ($multi) { + $this->cacheMock + ->expects($this->once()) + ->method('getItems') + ->with([$cacheItem->getKey()]) + ->willReturn([$key => $cacheItem]); + } else { + $this->cacheMock + ->expects($this->once()) + ->method('getItem') + ->with($cacheItem->getKey()) + ->willReturn($cacheItem); + } + + $innerHandlerMock = $this->createMock($this->getHandlerClassName()); + $this->persistenceHandlerMock + ->expects($this->once()) + ->method($handlerMethodName) + ->willReturn($innerHandlerMock); + + $innerHandlerMock + ->expects($this->once()) + ->method($method) + ->with(...$arguments) + ->willReturn($data); + + foreach ($additionalCalls as $additionalCall) { + $innerHandlerMock = $this->createMock($additionalCall[1]); + $this->persistenceHandlerMock + ->expects($this->once()) + ->method($additionalCall[0]) + ->willReturn($innerHandlerMock); + + $innerHandlerMock + ->expects($this->once()) + ->method($additionalCall[2]) + ->willReturn($additionalCall[3]); + } + + $this->cacheMock + ->expects($this->once()) + ->method('save') + ->with($cacheItem); + + $handler = $this->persistenceCacheHandler->$handlerMethodName(); + $return = call_user_func_array([$handler, $method], $arguments); + + $this->assertEquals($data, $return); + + // Assert use of tags would probably need custom logic as internal property is [$tag => $tag] value and we don't want to know that. + //$this->assertAttributeEquals([], 'tags', $cacheItem); + } +} diff --git a/eZ/Publish/Core/Persistence/Cache/Tests/InMemory/InMemoryCacheTest.php b/eZ/Publish/Core/Persistence/Cache/Tests/InMemory/InMemoryCacheTest.php new file mode 100644 index 00000000000..7c3619fdb62 --- /dev/null +++ b/eZ/Publish/Core/Persistence/Cache/Tests/InMemory/InMemoryCacheTest.php @@ -0,0 +1,183 @@ +cache = new InMemoryCache( + 3000, + 8 + ); + } + + /** + * Tear down test (properties). + */ + final protected function tearDown() + { + $this->cache->clear(); + + unset($this->cache); + unset($GLOBALS['override_time']); + parent::tearDown(); + } + + public function testGetByKey(): void + { + $this->assertNull($this->cache->get('first')); + + $obj = new \stdClass(); + $this->cache->setMulti([$obj], static function ($o) { return ['first']; }); + + $this->assertSame($obj, $this->cache->get('first')); + + // Test TTL + $GLOBALS['override_time'] = \microtime(true) + 4; + $this->assertNull($this->cache->get('first')); + } + + public function testGetBySecondaryIndex(): void + { + $this->assertNull($this->cache->get('first')); + $this->assertNull($this->cache->get('secondary')); + + $obj = new \stdClass(); + $this->cache->setMulti([$obj], static function ($o) { return ['first', 'secondary']; }); + + $this->assertSame($obj, $this->cache->get('first')); + $this->assertSame($obj, $this->cache->get('secondary')); + + // Test TTL + $GLOBALS['override_time'] = \microtime(true) + 4; + $this->assertNull($this->cache->get('first')); + $this->assertNull($this->cache->get('secondary')); + } + + public function testGetByList(): void + { + $this->assertNull($this->cache->get('first')); + $this->assertNull($this->cache->get('list')); + + $obj = new \stdClass(); + $this->cache->setMulti([$obj], static function ($o) { return ['first']; }, 'list'); + + $this->assertSame($obj, $this->cache->get('first')); + $this->assertSame([$obj], $this->cache->get('list')); + + // Test TTL + $GLOBALS['override_time'] = \microtime(true) + 4; + $this->assertNull($this->cache->get('first')); + $this->assertNull($this->cache->get('list')); + } + + public function testDeleted(): void + { + $obj = new \stdClass(); + $this->cache->setMulti([$obj], static function ($o) { return ['first', 'second']; }, 'list'); + + $this->assertSame($obj, $this->cache->get('first')); + $this->assertSame($obj, $this->cache->get('second')); + $this->assertSame([$obj], $this->cache->get('list')); + + // Delete primary, her we expect secondary index to also start returning null + $this->cache->deleteMulti(['first']); + + $this->assertNull($this->cache->get('first')); + $this->assertNull($this->cache->get('second')); + $this->assertSame([$obj], $this->cache->get('list')); + + // Delete list + $this->cache->deleteMulti(['list']); + + $this->assertNull($this->cache->get('list')); + } + + public function testClear(): void + { + $obj = new \stdClass(); + $this->cache->setMulti([$obj], static function ($o) { return ['first', 'second']; }, 'list'); + + $this->assertSame($obj, $this->cache->get('first')); + $this->assertSame($obj, $this->cache->get('second')); + $this->assertSame([$obj], $this->cache->get('list')); + + // Clear all cache + $this->cache->clear(); + + $this->assertNull($this->cache->get('first')); + $this->assertNull($this->cache->get('second')); + $this->assertNull($this->cache->get('list')); + } + + public function testSetWhenReachingSetLimit(): void + { + $obj = new \stdClass(); + $this->cache->setMulti([$obj, $obj], static function ($o) { return ['first', 'second']; }, 'list'); + + $this->assertNull($this->cache->get('first')); + $this->assertNull($this->cache->get('second')); + $this->assertNull($this->cache->get('list')); + } + + public function testSetWhenReachingTotalLimit(): void + { + $obj = new \stdClass(); + $this->cache->setMulti([$obj], static function ($o) { return ['first']; }); + $this->cache->setMulti([$obj], static function ($o) { return ['second']; }); + $this->cache->setMulti([$obj], static function ($o) { return ['third']; }); + $this->cache->setMulti([$obj], static function ($o) { return ['fourth']; }); + $this->cache->setMulti([$obj], static function ($o) { return ['fifth']; }); + $this->cache->setMulti([$obj], static function ($o) { return ['sixth']; }); + $this->cache->setMulti([$obj], static function ($o) { return ['seventh']; }); + $this->cache->setMulti([$obj], static function ($o) { return ['eight']; }); + + $this->assertNull($this->cache->get('first')); + $this->assertNull($this->cache->get('second')); + $this->assertSame($obj, $this->cache->get('third')); + $this->assertSame($obj, $this->cache->get('fourth')); + $this->assertSame($obj, $this->cache->get('fifth')); + $this->assertSame($obj, $this->cache->get('sixth')); + $this->assertSame($obj, $this->cache->get('seventh')); + $this->assertSame($obj, $this->cache->get('eight')); + } +} + +namespace eZ\Publish\Core\Persistence\Cache\InMemory; + +/** + * Overloads microtime(true) calls in InMemoryCache in order to be able to test expiry. + * + * @return float|string + */ +function microtime($asFloat = false) +{ + if ($asFloat & isset($GLOBALS['override_time'])) { + return $GLOBALS['override_time']; + } + + return \microtime($asFloat); +} diff --git a/eZ/Publish/Core/settings/storage_engines/cache.yml b/eZ/Publish/Core/settings/storage_engines/cache.yml index c13a87d3b9f..e2df043f161 100644 --- a/eZ/Publish/Core/settings/storage_engines/cache.yml +++ b/eZ/Publish/Core/settings/storage_engines/cache.yml @@ -10,10 +10,8 @@ parameters: ezpublish.spi.persistence.cache.contentLanguageHandler.class: eZ\Publish\Core\Persistence\Cache\ContentLanguageHandler ezpublish.spi.persistence.cache.contentTypeHandler.class: eZ\Publish\Core\Persistence\Cache\ContentTypeHandler ezpublish.spi.persistence.cache.userHandler.class: eZ\Publish\Core\Persistence\Cache\UserHandler - ezpublish.spi.persistence.cache.searchHandler.class: eZ\Publish\Core\Persistence\Cache\SearchHandler ezpublish.spi.persistence.cache.transactionhandler.class: eZ\Publish\Core\Persistence\Cache\TransactionHandler ezpublish.spi.persistence.cache.trashHandler.class: eZ\Publish\Core\Persistence\Cache\TrashHandler - ezpublish.spi.persistence.cache.locationSearchHandler.class: eZ\Publish\Core\Persistence\Cache\LocationSearchHandler ezpublish.spi.persistence.cache.urlAliasHandler.class: eZ\Publish\Core\Persistence\Cache\UrlAliasHandler ezpublish.spi.persistence.cache.objectStateHandler.class: eZ\Publish\Core\Persistence\Cache\ObjectStateHandler ezpublish.spi.persistence.cache.urlHandler.class: eZ\Publish\Core\Persistence\Cache\URLHandler @@ -23,6 +21,10 @@ parameters: ezpublish.spi.persistence.cache.persistenceLogger.class: eZ\Publish\Core\Persistence\Cache\PersistenceLogger # Make sure logging is only enabled for debug by default ezpublish.spi.persistence.cache.persistenceLogger.enableCallLogging: "%kernel.debug%" + # Global in-memory settings + ezpublish.spi.persistence.cache.inmemory.ttl: 3000 + ezpublish.spi.persistence.cache.inmemory.limit: 100 + ezpublish.spi.persistence.cache.inmemory.enable: true services: ezpublish.cache_pool: @@ -35,11 +37,22 @@ services: class: "%ezpublish.cache_pool.driver.class%" arguments: ["", 120] + # Logger which logs info when in dev for synfony web toolbar ezpublish.spi.persistence.cache.persistenceLogger: class: "%ezpublish.spi.persistence.cache.persistenceLogger.class%" arguments: - "%ezpublish.spi.persistence.cache.persistenceLogger.enableCallLogging%" + # In-Memory cache pools + ezpublish.spi.persistence.cache.inmemory: + class: eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache + arguments: + - "%ezpublish.spi.persistence.cache.inmemory.ttl%" + - "%ezpublish.spi.persistence.cache.inmemory.limit%" + - "%ezpublish.spi.persistence.cache.inmemory.enable%" + + + # SPI Persistence Cache Handlers, incl abstracts ezpublish.spi.persistence.cache.abstractHandler: class: "%ezpublish.spi.persistence.cache.abstractHandler.class%" abstract: true @@ -48,6 +61,15 @@ services: - "@ezpublish.api.storage_engine" - "@ezpublish.spi.persistence.cache.persistenceLogger" + ezpublish.spi.persistence.cache.abstractInMemoryCacheHandler: + class: eZ\Publish\Core\Persistence\Cache\AbstractInMemoryHandler + abstract: true + arguments: + - "@ezpublish.cache_pool" + - "@ezpublish.api.storage_engine" + - "@ezpublish.spi.persistence.cache.persistenceLogger" + - "@ezpublish.spi.persistence.cache.inmemory" + ezpublish.spi.persistence.cache.sectionHandler: class: "%ezpublish.spi.persistence.cache.sectionHandler.class%" parent: ezpublish.spi.persistence.cache.abstractHandler From 536133ed91ad984f31bb74fd596ea6de16754a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20R?= Date: Tue, 26 Feb 2019 15:00:46 +0100 Subject: [PATCH 05/17] Adapt ContentLanguageHandler to use InMemory cache --- .../Cache/ContentLanguageHandler.php | 135 +++++++++--------- .../Cache/Tests/AbstractBaseHandlerTest.php | 2 +- .../Tests/ContentLanguageHandlerTest.php | 14 +- .../Core/settings/storage_engines/cache.yml | 2 +- 4 files changed, 81 insertions(+), 72 deletions(-) diff --git a/eZ/Publish/Core/Persistence/Cache/ContentLanguageHandler.php b/eZ/Publish/Core/Persistence/Cache/ContentLanguageHandler.php index 5f32ac2c2c1..721d45d0d7a 100644 --- a/eZ/Publish/Core/Persistence/Cache/ContentLanguageHandler.php +++ b/eZ/Publish/Core/Persistence/Cache/ContentLanguageHandler.php @@ -8,22 +8,48 @@ */ namespace eZ\Publish\Core\Persistence\Cache; +use eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache; +use eZ\Publish\SPI\Persistence\Handler as PersistenceHandler; use eZ\Publish\SPI\Persistence\Content\Language\Handler as ContentLanguageHandlerInterface; use eZ\Publish\SPI\Persistence\Content\Language; use eZ\Publish\SPI\Persistence\Content\Language\CreateStruct; +use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface; /** * @see \eZ\Publish\SPI\Persistence\Content\Language\Handler */ -class ContentLanguageHandler extends AbstractHandler implements ContentLanguageHandlerInterface +class ContentLanguageHandler extends AbstractInMemoryHandler implements ContentLanguageHandlerInterface { + /** @var callable */ + private $getTags; + + /** @var callable */ + private $getKeys; + + public function __construct( + TagAwareAdapterInterface $cache, + PersistenceHandler $persistenceHandler, + PersistenceLogger $logger, + InMemoryCache $inMemory + ) { + parent::__construct($cache, $persistenceHandler, $logger, $inMemory); + + $this->getTags = static function (Language $language) { return ['language-' . $language->id]; }; + $this->getKeys = static function (Language $language) { + return [ + 'ez-language-' . $language->id, + 'ez-language-code-' . $language->languageCode, + ]; + }; + } + /** * {@inheritdoc} */ public function create(CreateStruct $struct) { $this->logger->logCall(__METHOD__, array('struct' => $struct)); - $this->cache->deleteItem('ez-language-list'); + $this->deleteCache(['ez-language-list']); return $this->persistenceHandler->contentLanguageHandler()->create($struct); } @@ -36,7 +62,11 @@ public function update(Language $struct) $this->logger->logCall(__METHOD__, array('struct' => $struct)); $return = $this->persistenceHandler->contentLanguageHandler()->update($struct); - $this->cache->invalidateTags(['language-' . $struct->id]); + $this->deleteCache([ + 'ez-language-list', + 'ez-language-' . $struct->id, + 'ez-language-code-' . $struct->languageCode, + ]); return $return; } @@ -46,19 +76,15 @@ public function update(Language $struct) */ public function load($id) { - $cacheItem = $this->cache->getItem('ez-language-' . $id); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('language' => $id)); - $language = $this->persistenceHandler->contentLanguageHandler()->load($id); - - $cacheItem->set($language); - $cacheItem->tag('language-' . $language->id); - $this->cache->save($cacheItem); - - return $language; + return $this->getCacheValue( + $id, + 'ez-language-', + function ($id) { + return $this->persistenceHandler->contentLanguageHandler()->load($id); + }, + $this->getTags, + $this->getKeys + ); } /** @@ -66,17 +92,14 @@ public function load($id) */ public function loadList(array $ids): iterable { - return $this->getMultipleCacheItems( + return $this->getMultipleCacheValues( $ids, 'ez-language-', - function (array $cacheMissIds) { - $this->logger->logCall(__CLASS__ . '::loadList', ['languages' => $cacheMissIds]); - - return $this->persistenceHandler->contentLanguageHandler()->loadList($cacheMissIds); + function (array $ids) { + return $this->persistenceHandler->contentLanguageHandler()->loadList($ids); }, - function (Language $language) { - return ['language-' . $language->id]; - } + $this->getTags, + $this->getKeys ); } @@ -85,19 +108,15 @@ function (Language $language) { */ public function loadByLanguageCode($languageCode) { - $cacheItem = $this->cache->getItem('ez-language-code-' . $languageCode); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('language' => $languageCode)); - $language = $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode($languageCode); - - $cacheItem->set($language); - $cacheItem->tag('language-' . $language->id); - $this->cache->save($cacheItem); - - return $language; + return $this->getCacheValue( + $languageCode, + 'ez-language-code-', + function ($languageCode) { + return $this->persistenceHandler->contentLanguageHandler()->loadByLanguageCode($languageCode); + }, + $this->getTags, + $this->getKeys + ); } /** @@ -105,17 +124,14 @@ public function loadByLanguageCode($languageCode) */ public function loadListByLanguageCodes(array $languageCodes): iterable { - return $this->getMultipleCacheItems( + return $this->getMultipleCacheValues( $languageCodes, 'ez-language-code-', - function (array $cacheMissIds) { - $this->logger->logCall(__CLASS__ . '::loadListByLanguageCodes', ['languages' => $cacheMissIds]); - - return $this->persistenceHandler->contentLanguageHandler()->loadListByLanguageCodes($cacheMissIds); + function (array $languageCodes) { + return $this->persistenceHandler->contentLanguageHandler()->loadListByLanguageCodes($languageCodes); }, - function (Language $language) { - return ['language-' . $language->id]; - } + $this->getTags, + $this->getKeys ); } @@ -124,24 +140,14 @@ function (Language $language) { */ public function loadAll() { - $cacheItem = $this->cache->getItem('ez-language-list'); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__); - $languages = $this->persistenceHandler->contentLanguageHandler()->loadAll(); - - $cacheTags = []; - foreach ($languages as $language) { - $cacheTags[] = 'language-' . $language->id; - } - - $cacheItem->set($languages); - $cacheItem->tag($cacheTags); - $this->cache->save($cacheItem); - - return $languages; + return $this->getListCacheValue( + 'ez-language-list', + function () { + return $this->persistenceHandler->contentLanguageHandler()->loadAll(); + }, + $this->getTags, + $this->getKeys + ); } /** @@ -152,7 +158,8 @@ public function delete($id) $this->logger->logCall(__METHOD__, array('language' => $id)); $return = $this->persistenceHandler->contentLanguageHandler()->delete($id); - $this->cache->invalidateTags(['language-' . $id]); + // As we don't have locale we clear cache by tag invalidation + $this->invalidateCache(['language-' . $id]); return $return; } diff --git a/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php b/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php index 936ba2d3a78..be9464e045d 100644 --- a/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php +++ b/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php @@ -82,7 +82,7 @@ final protected function setUp() new CacheSectionHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), new CacheLocationHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), new CacheContentHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), - new CacheContentLanguageHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), + new CacheContentLanguageHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock, $this->inMemoryMock), new CacheContentTypeHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), new CacheUserHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), new CacheTransactionHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), diff --git a/eZ/Publish/Core/Persistence/Cache/Tests/ContentLanguageHandlerTest.php b/eZ/Publish/Core/Persistence/Cache/Tests/ContentLanguageHandlerTest.php index d171fa81b5b..708839a5ea2 100644 --- a/eZ/Publish/Core/Persistence/Cache/Tests/ContentLanguageHandlerTest.php +++ b/eZ/Publish/Core/Persistence/Cache/Tests/ContentLanguageHandlerTest.php @@ -15,7 +15,7 @@ /** * Test case for Persistence\Cache\ContentLanguageHandler. */ -class ContentLanguageHandlerTest extends AbstractCacheHandlerTest +class ContentLanguageHandlerTest extends AbstractInMemoryCacheHandlerTest { public function getHandlerMethodName(): string { @@ -29,23 +29,25 @@ public function getHandlerClassName(): string public function providerForUnCachedMethods(): array { - // string $method, array $arguments, array? $tags, string? $key + $language = new SPILanguage(['id' => 5, 'languageCode' => 'eng-GB']); + + // string $method, array $arguments, array? $tags, array? $key return [ - ['create', [new SPILanguageCreateStruct()]], - ['update', [new SPILanguage(['id' => 5])], ['language-5']], - ['loadAll', []], + ['create', [new SPILanguageCreateStruct()], null, ['ez-language-list']], + ['update', [$language], null, ['ez-language-list', 'ez-language-5', 'ez-language-code-eng-GB']], ['delete', [5], ['language-5']], ]; } public function providerForCachedLoadMethods(): array { - $object = new SPILanguage(['id' => 5]); + $object = new SPILanguage(['id' => 5, 'languageCode' => 'eng-GB']); // string $method, array $arguments, string $key, mixed? $data, bool $multi return [ ['load', [5], 'ez-language-5', $object], ['loadList', [[5]], 'ez-language-5', [5 => $object], true], + ['loadAll', [], 'ez-language-list', [5 => $object], false], ['loadByLanguageCode', ['eng-GB'], 'ez-language-code-eng-GB', $object], ['loadListByLanguageCodes', [['eng-GB']], 'ez-language-code-eng-GB', ['eng-GB' => $object], true], ]; diff --git a/eZ/Publish/Core/settings/storage_engines/cache.yml b/eZ/Publish/Core/settings/storage_engines/cache.yml index e2df043f161..ccc773510f3 100644 --- a/eZ/Publish/Core/settings/storage_engines/cache.yml +++ b/eZ/Publish/Core/settings/storage_engines/cache.yml @@ -84,7 +84,7 @@ services: ezpublish.spi.persistence.cache.contentLanguageHandler: class: "%ezpublish.spi.persistence.cache.contentLanguageHandler.class%" - parent: ezpublish.spi.persistence.cache.abstractHandler + parent: ezpublish.spi.persistence.cache.abstractInMemoryCacheHandler ezpublish.spi.persistence.cache.contentTypeHandler: class: "%ezpublish.spi.persistence.cache.contentTypeHandler.class%" From 6efc2b99105d02b1db3dc7088fff885bfc3ab9d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20R?= Date: Wed, 27 Feb 2019 13:51:55 +0100 Subject: [PATCH 06/17] Adapt ContentTypeHandler to use InMemory cache --- .../Persistence/Cache/ContentTypeHandler.php | 322 +++++++++--------- .../Cache/Tests/AbstractBaseHandlerTest.php | 2 +- .../Cache/Tests/ContentTypeHandlerTest.php | 37 +- .../Core/settings/storage_engines/cache.yml | 2 +- 4 files changed, 187 insertions(+), 176 deletions(-) diff --git a/eZ/Publish/Core/Persistence/Cache/ContentTypeHandler.php b/eZ/Publish/Core/Persistence/Cache/ContentTypeHandler.php index 865be6b9be1..350247aa694 100644 --- a/eZ/Publish/Core/Persistence/Cache/ContentTypeHandler.php +++ b/eZ/Publish/Core/Persistence/Cache/ContentTypeHandler.php @@ -8,6 +8,8 @@ */ namespace eZ\Publish\Core\Persistence\Cache; +use eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache; +use eZ\Publish\SPI\Persistence\Handler as PersistenceHandler; use eZ\Publish\SPI\Persistence\Content\Type\Handler as ContentTypeHandlerInterface; use eZ\Publish\SPI\Persistence\Content\Type; use eZ\Publish\SPI\Persistence\Content\Type\CreateStruct; @@ -15,6 +17,7 @@ use eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition; use eZ\Publish\SPI\Persistence\Content\Type\Group\CreateStruct as GroupCreateStruct; use eZ\Publish\SPI\Persistence\Content\Type\Group\UpdateStruct as GroupUpdateStruct; +use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface; /** * ContentType cache. @@ -23,15 +26,53 @@ * * @see \eZ\Publish\SPI\Persistence\Content\Type\Handler */ -class ContentTypeHandler extends AbstractHandler implements ContentTypeHandlerInterface +class ContentTypeHandler extends AbstractInMemoryHandler implements ContentTypeHandlerInterface { + /** @var callable */ + private $getGroupTags; + + /** @var callable */ + private $getGroupKeys; + + /** @var callable */ + private $getTypeTags; + + /** @var callable */ + private $getTypeKeys; + + public function __construct( + TagAwareAdapterInterface $cache, + PersistenceHandler $persistenceHandler, + PersistenceLogger $logger, + InMemoryCache $inMemory + ) { + parent::__construct($cache, $persistenceHandler, $logger, $inMemory); + + $this->getGroupTags = static function (Type\Group $group) { return ['type-group-' . $group->id]; }; + $this->getGroupKeys = static function (Type\Group $group) { + return [ + 'ez-content-type-group-' . $group->id, + 'ez-content-type-group-' . $group->identifier . '-by-identifier', + ]; + }; + + $this->getTypeTags = static function (Type $type) { return ['type-' . $type->id]; }; + $this->getTypeKeys = static function (Type $type, int $status = Type::STATUS_DEFINED) { + return [ + 'ez-content-type-' . $type->id . '-' . $status, + 'ez-content-type-' . $type->identifier . '-by-identifier', + 'ez-content-type-' . $type->remoteId . '-by-remote', + ]; + }; + } + /** * {@inheritdoc} */ public function createGroup(GroupCreateStruct $struct) { $this->logger->logCall(__METHOD__, array('struct' => $struct)); - $this->cache->deleteItem('ez-content-type-group-list'); + $this->deleteCache(['ez-content-type-group-list']); return $this->persistenceHandler->contentTypeHandler()->createGroup($struct); } @@ -44,7 +85,11 @@ public function updateGroup(GroupUpdateStruct $struct) $this->logger->logCall(__METHOD__, array('struct' => $struct)); $group = $this->persistenceHandler->contentTypeHandler()->updateGroup($struct); - $this->cache->invalidateTags(['type-group-' . $struct->id]); + $this->deleteCache([ + 'ez-content-type-group-list', + 'ez-content-type-group-' . $struct->id, + 'ez-content-type-group-' . $struct->identifier . '-by-identifier', + ]); return $group; } @@ -57,7 +102,7 @@ public function deleteGroup($groupId) $this->logger->logCall(__METHOD__, array('group' => $groupId)); $return = $this->persistenceHandler->contentTypeHandler()->deleteGroup($groupId); - $this->cache->invalidateTags(['type-group-' . $groupId]); + $this->invalidateCache(['type-group-' . $groupId]); return $return; } @@ -67,19 +112,15 @@ public function deleteGroup($groupId) */ public function loadGroup($groupId) { - $cacheItem = $this->cache->getItem('ez-content-type-group-' . $groupId); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('group' => $groupId)); - $group = $this->persistenceHandler->contentTypeHandler()->loadGroup($groupId); - - $cacheItem->set($group); - $cacheItem->tag('type-group-' . $group->id); - $this->cache->save($cacheItem); - - return $group; + return $this->getCacheValue( + $groupId, + 'ez-content-type-group-', + function ($groupId) { + return $this->persistenceHandler->contentTypeHandler()->loadGroup($groupId); + }, + $this->getGroupTags, + $this->getGroupKeys + ); } /** @@ -87,17 +128,14 @@ public function loadGroup($groupId) */ public function loadGroups(array $groupIds) { - return $this->getMultipleCacheItems( + return $this->getMultipleCacheValues( $groupIds, 'ez-content-type-group-', - function (array $cacheMissIds) { - $this->logger->logCall(__CLASS__ . '::loadGroups', ['groups' => $cacheMissIds]); - - return $this->persistenceHandler->contentTypeHandler()->loadGroups($cacheMissIds); + function (array $groupIds) { + return $this->persistenceHandler->contentTypeHandler()->loadGroups($groupIds); }, - function (Type\Group $group) { - return ['type-group-' . $group->id]; - } + $this->getGroupTags, + $this->getGroupKeys ); } @@ -106,19 +144,16 @@ function (Type\Group $group) { */ public function loadGroupByIdentifier($identifier) { - $cacheItem = $this->cache->getItem('ez-content-type-group-' . $identifier . '-by-identifier'); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('group' => $identifier)); - $group = $this->persistenceHandler->contentTypeHandler()->loadGroupByIdentifier($identifier); - - $cacheItem->set($group); - $cacheItem->tag('type-group-' . $group->id); - $this->cache->save($cacheItem); - - return $group; + return $this->getCacheValue( + $identifier, + 'ez-content-type-group-', + function (string $identifier) { + return $this->persistenceHandler->contentTypeHandler()->loadGroupByIdentifier($identifier); + }, + $this->getGroupTags, + $this->getGroupKeys, + '-by-identifier' + ); } /** @@ -126,24 +161,14 @@ public function loadGroupByIdentifier($identifier) */ public function loadAllGroups() { - $cacheItem = $this->cache->getItem('ez-content-type-group-list'); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__); - $groups = $this->persistenceHandler->contentTypeHandler()->loadAllGroups(); - - $cacheTags = []; - foreach ($groups as $group) { - $cacheTags[] = 'type-group-' . $group->id; - } - - $cacheItem->set($groups); - $cacheItem->tag($cacheTags); - $this->cache->save($cacheItem); - - return $groups; + return $this->getListCacheValue( + 'ez-content-type-group-list', + function () { + return $this->persistenceHandler->contentTypeHandler()->loadAllGroups(); + }, + $this->getGroupTags, + $this->getGroupKeys + ); } /** @@ -157,40 +182,29 @@ public function loadContentTypes($groupId, $status = Type::STATUS_DEFINED) return $this->persistenceHandler->contentTypeHandler()->loadContentTypes($groupId, $status); } - $cacheItem = $this->cache->getItem('ez-content-type-list-by-group-' . $groupId); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('group' => $groupId, 'status' => $status)); - $types = $this->persistenceHandler->contentTypeHandler()->loadContentTypes($groupId, $status); - - $cacheTags = ['type-group-' . $groupId]; - foreach ($types as $type) { - $cacheTags[] = 'type-' . $type->id; - } - - $cacheItem->set($types); - $cacheItem->tag($cacheTags); - $this->cache->save($cacheItem); - - return $types; + return $this->getListCacheValue( + 'ez-content-type-list-by-group-' . $groupId, + function () use ($groupId, $status) { + return $this->persistenceHandler->contentTypeHandler()->loadContentTypes($groupId, $status); + }, + $this->getTypeTags, + $this->getTypeKeys, + ['type-group-' . $groupId], + [$groupId] + ); } public function loadContentTypeList(array $contentTypeIds): array { - return $this->getMultipleCacheItems( + return $this->getMultipleCacheValues( $contentTypeIds, 'ez-content-type-', - function (array $cacheMissIds) { - $this->logger->logCall(__CLASS__ . '::loadContentTypeList', ['type' => $cacheMissIds]); - - return $this->persistenceHandler->contentTypeHandler()->loadContentTypeList($cacheMissIds); - }, - function (Type $type) { - return ['type-' . $type->id]; + function (array $contentTypeIds) { + return $this->persistenceHandler->contentTypeHandler()->loadContentTypeList($contentTypeIds); }, - array_fill_keys($contentTypeIds, '-' . Type::STATUS_DEFINED) + $this->getTypeTags, + $this->getTypeKeys, + '-' . Type::STATUS_DEFINED ); } @@ -199,19 +213,20 @@ function (Type $type) { */ public function load($typeId, $status = Type::STATUS_DEFINED) { - $cacheItem = $this->cache->getItem('ez-content-type-' . $typeId . '-' . $status); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('type' => $typeId, 'status' => $status)); - $type = $this->persistenceHandler->contentTypeHandler()->load($typeId, $status); + $getTypeKeysFn = $this->getTypeKeys; - $cacheItem->set($type); - $cacheItem->tag(['type-' . $type->id]); - $this->cache->save($cacheItem); - - return $type; + return $this->getCacheValue( + $typeId, + 'ez-content-type-', + function ($typeId) use ($status) { + return $this->persistenceHandler->contentTypeHandler()->load($typeId, $status); + }, + $this->getTypeTags, + static function (Type $type) use ($status, $getTypeKeysFn) { + return $getTypeKeysFn($type, $status); + }, + '-' . $status + ); } /** @@ -219,19 +234,16 @@ public function load($typeId, $status = Type::STATUS_DEFINED) */ public function loadByIdentifier($identifier) { - $cacheItem = $this->cache->getItem('ez-content-type-' . $identifier . '-by-identifier'); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('type' => $identifier)); - $type = $this->persistenceHandler->contentTypeHandler()->loadByIdentifier($identifier); - - $cacheItem->set($type); - $cacheItem->tag(['type-' . $type->id]); - $this->cache->save($cacheItem); - - return $type; + return $this->getCacheValue( + $identifier, + 'ez-content-type-', + function ($identifier) { + return $this->persistenceHandler->contentTypeHandler()->loadByIdentifier($identifier); + }, + $this->getTypeTags, + $this->getTypeKeys, + '-by-identifier' + ); } /** @@ -239,19 +251,16 @@ public function loadByIdentifier($identifier) */ public function loadByRemoteId($remoteId) { - $cacheItem = $this->cache->getItem('ez-content-type-' . $remoteId . '-by-remote'); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('type' => $remoteId)); - $type = $this->persistenceHandler->contentTypeHandler()->loadByRemoteId($remoteId); - - $cacheItem->set($type); - $cacheItem->tag(['type-' . $type->id]); - $this->cache->save($cacheItem); - - return $type; + return $this->getCacheValue( + $remoteId, + 'ez-content-type-', + function ($remoteId) { + return $this->persistenceHandler->contentTypeHandler()->loadByRemoteId($remoteId); + }, + $this->getTypeTags, + $this->getTypeKeys, + '-by-remote' + ); } /** @@ -264,9 +273,12 @@ public function create(CreateStruct $struct) $type = $this->persistenceHandler->contentTypeHandler()->create($struct); // Clear loadContentTypes() cache as we effetely add an item to it's collection here. - foreach ($struct->groupIds as $groupId) { - $this->cache->deleteItem('ez-content-type-list-by-group-' . $groupId); - } + $this->deleteCache(array_map( + function ($groupId) { + return 'ez-content-type-list-by-group-' . $groupId; + }, + $struct->groupIds + )); return $type; } @@ -280,9 +292,9 @@ public function update($typeId, $status, UpdateStruct $struct) $type = $this->persistenceHandler->contentTypeHandler()->update($typeId, $status, $struct); if ($status === Type::STATUS_DEFINED) { - $this->cache->invalidateTags(['type-' . $typeId, 'type-map', 'content-fields-type-' . $typeId]); + $this->invalidateCache(['type-' . $typeId, 'type-map', 'content-fields-type-' . $typeId]); } else { - $this->cache->deleteItem('ez-content-type-' . $typeId . '-' . $status); + $this->deleteCache(['ez-content-type-' . $typeId . '-' . $status]); } return $type; @@ -297,9 +309,9 @@ public function delete($typeId, $status) $return = $this->persistenceHandler->contentTypeHandler()->delete($typeId, $status); if ($status === Type::STATUS_DEFINED) { - $this->cache->invalidateTags(['type-' . $typeId, 'type-map', 'content-fields-type-' . $typeId]); + $this->invalidateCache(['type-' . $typeId, 'type-map', 'content-fields-type-' . $typeId]); } else { - $this->cache->deleteItem('ez-content-type-' . $typeId . '-' . $status); + $this->deleteCache(['ez-content-type-' . $typeId . '-' . $status]); } return $return; @@ -313,7 +325,7 @@ public function createDraft($modifierId, $typeId) $this->logger->logCall(__METHOD__, array('modifier' => $modifierId, 'type' => $typeId)); $draft = $this->persistenceHandler->contentTypeHandler()->createDraft($modifierId, $typeId); - $this->cache->deleteItem('ez-content-type-' . $typeId . '-' . Type::STATUS_DRAFT); + $this->deleteCache(['ez-content-type-' . $typeId . '-' . Type::STATUS_DRAFT]); return $draft; } @@ -337,9 +349,9 @@ public function unlink($groupId, $typeId, $status) $return = $this->persistenceHandler->contentTypeHandler()->unlink($groupId, $typeId, $status); if ($status === Type::STATUS_DEFINED) { - $this->cache->invalidateTags(['type-' . $typeId]); + $this->invalidateCache(['type-' . $typeId]); } else { - $this->cache->deleteItem('ez-content-type-' . $typeId . '-' . $status); + $this->deleteCache(['ez-content-type-' . $typeId . '-' . $status]); } return $return; @@ -354,11 +366,11 @@ public function link($groupId, $typeId, $status) $return = $this->persistenceHandler->contentTypeHandler()->link($groupId, $typeId, $status); if ($status === Type::STATUS_DEFINED) { - $this->cache->invalidateTags(['type-' . $typeId]); + $this->invalidateCache(['type-' . $typeId]); // Clear loadContentTypes() cache as we effetely add an item to it's collection here. - $this->cache->deleteItem('ez-content-type-list-by-group-' . $groupId); + $this->deleteCache(['ez-content-type-list-by-group-' . $groupId]); } else { - $this->cache->deleteItem('ez-content-type-' . $typeId . '-' . $status); + $this->deleteCache(['ez-content-type-' . $typeId . '-' . $status]); } return $return; @@ -397,9 +409,9 @@ public function addFieldDefinition($typeId, $status, FieldDefinition $struct) ); if ($status === Type::STATUS_DEFINED) { - $this->cache->invalidateTags(['type-' . $typeId, 'type-map', 'content-fields-type-' . $typeId]); + $this->invalidateCache(['type-' . $typeId, 'type-map', 'content-fields-type-' . $typeId]); } else { - $this->cache->deleteItem('ez-content-type-' . $typeId . '-' . $status); + $this->deleteCache(['ez-content-type-' . $typeId . '-' . $status]); } return $return; @@ -418,9 +430,9 @@ public function removeFieldDefinition($typeId, $status, $fieldDefinitionId) ); if ($status === Type::STATUS_DEFINED) { - $this->cache->invalidateTags(['type-' . $typeId, 'type-map', 'content-fields-type-' . $typeId]); + $this->invalidateCache(['type-' . $typeId, 'type-map', 'content-fields-type-' . $typeId]); } else { - $this->cache->deleteItem('ez-content-type-' . $typeId . '-' . $status); + $this->deleteCache(['ez-content-type-' . $typeId . '-' . $status]); } } @@ -437,9 +449,9 @@ public function updateFieldDefinition($typeId, $status, FieldDefinition $struct) ); if ($status === Type::STATUS_DEFINED) { - $this->cache->invalidateTags(['type-' . $typeId, 'type-map', 'content-fields-type-' . $typeId]); + $this->invalidateCache(['type-' . $typeId, 'type-map', 'content-fields-type-' . $typeId]); } else { - $this->cache->deleteItem('ez-content-type-' . $typeId . '-' . $status); + $this->deleteCache(['ez-content-type-' . $typeId . '-' . $status]); } } @@ -452,11 +464,11 @@ public function publish($typeId) $this->persistenceHandler->contentTypeHandler()->publish($typeId); // Clear type cache, map cache, and content cache which contains fields. - $this->cache->invalidateTags(['type-' . $typeId, 'type-map', 'content-fields-type-' . $typeId]); + $this->invalidateCache(['type-' . $typeId, 'type-map', 'content-fields-type-' . $typeId]); // Clear Content Type Groups list cache - $contentType = $this->load($typeId, Type::STATUS_DEFINED); - $this->cache->deleteItems( + $contentType = $this->load($typeId); + $this->deleteCache( array_map( function ($groupId) { return 'ez-content-type-list-by-group-' . $groupId; @@ -471,19 +483,15 @@ function ($groupId) { */ public function getSearchableFieldMap() { - $cacheItem = $this->cache->getItem('ez-content-type-field-map'); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__); - $fieldMap = $this->persistenceHandler->contentTypeHandler()->getSearchableFieldMap(); - - $cacheItem->set($fieldMap); - $cacheItem->tag(['type-map']); - $this->cache->save($cacheItem); - - return $fieldMap; + return $this->getListCacheValue( + 'ez-content-type-field-map', + function () { + return $this->persistenceHandler->contentTypeHandler()->getSearchableFieldMap(); + }, + static function () {return [];}, + static function () {return [];}, + ['type-map'] + ); } /** @@ -502,7 +510,7 @@ public function removeContentTypeTranslation(int $contentTypeId, string $languag $languageCode ); - $this->cache->invalidateTags(['type-' . $contentTypeId, 'type-map', 'content-fields-type-' . $contentTypeId]); + $this->invalidateCache(['type-' . $contentTypeId, 'type-map', 'content-fields-type-' . $contentTypeId]); return $return; } diff --git a/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php b/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php index be9464e045d..c36e852a1de 100644 --- a/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php +++ b/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php @@ -83,7 +83,7 @@ final protected function setUp() new CacheLocationHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), new CacheContentHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), new CacheContentLanguageHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock, $this->inMemoryMock), - new CacheContentTypeHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), + new CacheContentTypeHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock, $this->inMemoryMock), new CacheUserHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), new CacheTransactionHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), new CacheTrashHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), diff --git a/eZ/Publish/Core/Persistence/Cache/Tests/ContentTypeHandlerTest.php b/eZ/Publish/Core/Persistence/Cache/Tests/ContentTypeHandlerTest.php index e5feb4c6b3f..b28d77c1ac5 100644 --- a/eZ/Publish/Core/Persistence/Cache/Tests/ContentTypeHandlerTest.php +++ b/eZ/Publish/Core/Persistence/Cache/Tests/ContentTypeHandlerTest.php @@ -20,7 +20,7 @@ /** * Test case for Persistence\Cache\ContentTypeHandler. */ -class ContentTypeHandlerTest extends AbstractCacheHandlerTest +class ContentTypeHandlerTest extends AbstractInMemoryCacheHandlerTest { public function getHandlerMethodName(): string { @@ -37,34 +37,36 @@ public function getHandlerClassName(): string */ public function providerForUnCachedMethods(): array { - // string $method, array $arguments, array? $tags, string? $key, mixed? $returnValue + $groupUpdate = new SPITypeGroupUpdateStruct(['id' => 3, 'identifier' => 'media']); + $typeUpdate = new SPITypeUpdateStruct(['identifier' => 'article', 'remoteId' => '34o9tj8394t']); + + // string $method, array $arguments, array? $tags, array? $key, mixed? $returnValue return [ - ['createGroup', [new SPITypeGroupCreateStruct()]], - ['updateGroup', [new SPITypeGroupUpdateStruct(['id' => 3])], ['type-group-3']], + ['createGroup', [new SPITypeGroupCreateStruct()], null, ['ez-content-type-group-list']], + ['updateGroup', [$groupUpdate], null, ['ez-content-type-group-list', 'ez-content-type-group-3', 'ez-content-type-group-media-by-identifier']], ['deleteGroup', [3], ['type-group-3']], - ['loadAllGroups', []], ['loadContentTypes', [3, 1]], // also listed for cached cases in providerForCachedLoadMethods ['create', [new SPITypeCreateStruct()]], - ['update', [5, 0, new SPITypeUpdateStruct()], ['type-5', 'type-map', 'content-fields-type-5']], - ['update', [5, 1, new SPITypeUpdateStruct()], [], 'ez-content-type-5-1'], + ['update', [5, 0, $typeUpdate], ['type-5', 'type-map', 'content-fields-type-5']], + ['update', [5, 1, $typeUpdate], null, ['ez-content-type-5-1']], ['delete', [5, 0], ['type-5', 'type-map', 'content-fields-type-5']], - ['delete', [5, 1], null, 'ez-content-type-5-1'], - ['createDraft', [10, 5], null, 'ez-content-type-5-1'], + ['delete', [5, 1], null, ['ez-content-type-5-1']], + ['createDraft', [10, 5], null, ['ez-content-type-5-1']], ['copy', [10, 5, 0]], ['copy', [10, 5, 1]], ['unlink', [3, 5, 0], ['type-5']], - ['unlink', [3, 5, 1], [], 'ez-content-type-5-1'], - ['link', [3, 5, 0], ['type-5'], 'ez-content-type-list-by-group-3'], - ['link', [3, 5, 1], null, 'ez-content-type-5-1'], + ['unlink', [3, 5, 1], null, ['ez-content-type-5-1']], + ['link', [3, 5, 0], ['type-5'], ['ez-content-type-list-by-group-3']], + ['link', [3, 5, 1], null, ['ez-content-type-5-1']], ['getFieldDefinition', [7, 1]], ['getFieldDefinition', [7, 0]], ['getContentCount', [5]], ['addFieldDefinition', [5, 0, new SPITypeFieldDefinition()], ['type-5', 'type-map', 'content-fields-type-5']], - ['addFieldDefinition', [5, 1, new SPITypeFieldDefinition()], [], 'ez-content-type-5-1'], + ['addFieldDefinition', [5, 1, new SPITypeFieldDefinition()], null, ['ez-content-type-5-1']], ['removeFieldDefinition', [5, 0, 7], ['type-5', 'type-map', 'content-fields-type-5']], - ['removeFieldDefinition', [5, 1, 7], null, 'ez-content-type-5-1'], + ['removeFieldDefinition', [5, 1, 7], null, ['ez-content-type-5-1']], ['updateFieldDefinition', [5, 0, new SPITypeFieldDefinition()], ['type-5', 'type-map', 'content-fields-type-5']], - ['updateFieldDefinition', [5, 1, new SPITypeFieldDefinition()], [], 'ez-content-type-5-1'], + ['updateFieldDefinition', [5, 1, new SPITypeFieldDefinition()], null, ['ez-content-type-5-1']], ['removeContentTypeTranslation', [5, 'eng-GB'], ['type-5', 'type-map', 'content-fields-type-5'], null, new SPIType()], ]; } @@ -74,14 +76,15 @@ public function providerForUnCachedMethods(): array */ public function providerForCachedLoadMethods(): array { - $group = new SPITypeGroup(['id' => 3]); - $type = new SPIType(['id' => 5]); + $group = new SPITypeGroup(['id' => 3, 'identifier' => 'media']); + $type = new SPIType(['id' => 5, 'identifier' => 'article', 'remoteId' => '34o9tj8394t']); // string $method, array $arguments, string $key, mixed? $data, bool? $multi, array? $additionalCalls return [ ['loadGroup', [3], 'ez-content-type-group-3', $group], ['loadGroups', [[3]], 'ez-content-type-group-3', [3 => $group], true], ['loadGroupByIdentifier', ['content'], 'ez-content-type-group-content-by-identifier', $group], + ['loadAllGroups', [], 'ez-content-type-group-list', [3 => $group]], ['loadContentTypes', [3, 0], 'ez-content-type-list-by-group-3', [$type]], ['loadContentTypeList', [[5]], 'ez-content-type-5-0', [5 => $type], true], ['load', [5, 0], 'ez-content-type-5-0', $type], diff --git a/eZ/Publish/Core/settings/storage_engines/cache.yml b/eZ/Publish/Core/settings/storage_engines/cache.yml index ccc773510f3..e276e4d5267 100644 --- a/eZ/Publish/Core/settings/storage_engines/cache.yml +++ b/eZ/Publish/Core/settings/storage_engines/cache.yml @@ -88,7 +88,7 @@ services: ezpublish.spi.persistence.cache.contentTypeHandler: class: "%ezpublish.spi.persistence.cache.contentTypeHandler.class%" - parent: ezpublish.spi.persistence.cache.abstractHandler + parent: ezpublish.spi.persistence.cache.abstractInMemoryCacheHandler ezpublish.spi.persistence.cache.userHandler: class: "%ezpublish.spi.persistence.cache.userHandler.class%" From ff6d4fa51fb6cd42bb4e5f0e34072387f94c3cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20R?= Date: Wed, 27 Feb 2019 16:51:11 +0100 Subject: [PATCH 07/17] Adapt TransactionHandlers in SPI Cache --- .../Persistence/Cache/Tests/AbstractBaseHandlerTest.php | 2 +- eZ/Publish/Core/Persistence/Cache/TransactionHandler.php | 9 ++++++--- eZ/Publish/Core/settings/storage_engines/cache.yml | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php b/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php index c36e852a1de..77684498bd2 100644 --- a/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php +++ b/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php @@ -85,7 +85,7 @@ final protected function setUp() new CacheContentLanguageHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock, $this->inMemoryMock), new CacheContentTypeHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock, $this->inMemoryMock), new CacheUserHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), - new CacheTransactionHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), + new CacheTransactionHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock, $this->inMemoryMock), new CacheTrashHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), new CacheUrlAliasHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), new CacheObjectStateHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), diff --git a/eZ/Publish/Core/Persistence/Cache/TransactionHandler.php b/eZ/Publish/Core/Persistence/Cache/TransactionHandler.php index c0277260963..0cd3fdb3f82 100644 --- a/eZ/Publish/Core/Persistence/Cache/TransactionHandler.php +++ b/eZ/Publish/Core/Persistence/Cache/TransactionHandler.php @@ -13,11 +13,14 @@ /** * Persistence Transaction Cache Handler class. */ -class TransactionHandler extends AbstractHandler implements TransactionHandlerInterface +class TransactionHandler extends AbstractInMemoryHandler implements TransactionHandlerInterface { /** * @todo Maybe this can be solved by contributing to Symfony, as in for instance using a layered cache with memory - * cache first and use saveDefered so cache is not persisted before commit is made, and ommited on rollback. + * cache first and use saveDefered so cache is not persisted before commit is made, and omitted on rollback. + * + * Or if we can get a checksum /fingerprint from cache pool which changes on actually cache commit so we can + * keep track to see if it has changed (to know if it is enough to clear inMemory cache + cache pool defer que) * * {@inheritdoc} */ @@ -42,7 +45,7 @@ public function commit() public function rollback() { $this->logger->logCall(__METHOD__); - $this->cache->clear(); // TIMBER!! @see beginTransaction() + $this->clearCache(); // TIMBER!! @see beginTransaction() $this->persistenceHandler->transactionHandler()->rollback(); } } diff --git a/eZ/Publish/Core/settings/storage_engines/cache.yml b/eZ/Publish/Core/settings/storage_engines/cache.yml index e276e4d5267..cdba0c07b9c 100644 --- a/eZ/Publish/Core/settings/storage_engines/cache.yml +++ b/eZ/Publish/Core/settings/storage_engines/cache.yml @@ -96,7 +96,7 @@ services: ezpublish.spi.persistence.cache.transactionhandler: class: "%ezpublish.spi.persistence.cache.transactionhandler.class%" - parent: ezpublish.spi.persistence.cache.abstractHandler + parent: ezpublish.spi.persistence.cache.abstractInMemoryCacheHandler ezpublish.spi.persistence.cache.trashHandler: class: "%ezpublish.spi.persistence.cache.trashHandler.class%" From 2792ed8ec1d7be8cab107c8658e496cd8ad89858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20R?= Date: Tue, 26 Feb 2019 15:07:24 +0100 Subject: [PATCH 08/17] Change Legacy Storage Engine language change to reuse InMemoryCache --- .../Legacy/Content/Language/Cache.php | 152 -------------- .../Content/Language/CachingHandler.php | 120 +++++++---- .../Tests/Content/Language/CacheTest.php | 187 ------------------ .../Language/CachingLanguageHandlerTest.php | 106 +++++----- .../storage_engines/legacy/language.yml | 6 +- 5 files changed, 124 insertions(+), 447 deletions(-) delete mode 100644 eZ/Publish/Core/Persistence/Legacy/Content/Language/Cache.php delete mode 100644 eZ/Publish/Core/Persistence/Legacy/Tests/Content/Language/CacheTest.php diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Language/Cache.php b/eZ/Publish/Core/Persistence/Legacy/Content/Language/Cache.php deleted file mode 100644 index 0e08fcd24d3..00000000000 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Language/Cache.php +++ /dev/null @@ -1,152 +0,0 @@ -mapById[$language->id] = $language; - $this->mapByLocale[$language->languageCode] = $language; - } - - /** - * Removes the language with $id from the cache. - * - * @param mixed $id - */ - public function remove($id) - { - unset($this->mapById[$id]); - foreach ($this->mapByLocale as $languageCode => $language) { - if ($language->id == $id) { - unset($this->mapByLocale[$languageCode]); - } - } - } - - /** - * Returns the Language with $id from the cache. - * - * @param mixed $id - * - * @return \eZ\Publish\SPI\Persistence\Content\Language - * - * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - * if the Language could not be found - */ - public function getById($id) - { - if (!isset($this->mapById[$id])) { - throw new NotFoundException('Language', $id); - } - - return $this->mapById[$id]; - } - - /** - * Returns Languages with $ids from the cache. - * - * @param int[] $ids - * - * @return \eZ\Publish\SPI\Persistence\Content\Language[]|iterable - */ - public function getListById(array $ids): iterable - { - $languages = []; - foreach ($ids as $id) { - if (isset($this->mapById[$id])) { - $languages[$id] = $this->mapById[$id]; - } - } - - return $languages; - } - - /** - * Returns the Language with $languageCode from the cache. - * - * @param string $languageCode - * - * @return \eZ\Publish\SPI\Persistence\Content\Language - * - * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException - * if the Language could not be found - */ - public function getByLocale($languageCode) - { - if (!isset($this->mapByLocale[$languageCode])) { - throw new NotFoundException('Language', $languageCode); - } - - return $this->mapByLocale[$languageCode]; - } - - /** - * Returns Languages with $languageCodes from the cache. - * - * @param string[] $languageCodes - * - * @return \eZ\Publish\SPI\Persistence\Content\Language[]|iterable - */ - public function getListByLocale(array $languageCodes): iterable - { - $languages = []; - foreach ($languageCodes as $languageCode) { - if (isset($this->mapByLocale[$languageCode])) { - $languages[$languageCode] = $this->mapByLocale[$languageCode]; - } - } - - return $languages; - } - - /** - * Returns all languages in the cache with locale as key. - * - * @return \eZ\Publish\SPI\Persistence\Content\Language[] - */ - public function getAll() - { - return $this->mapByLocale; - } - - /** - * CLear language cache. - */ - public function clearCache() - { - $this->mapByLocale = $this->mapById = array(); - } -} diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Language/CachingHandler.php b/eZ/Publish/Core/Persistence/Legacy/Content/Language/CachingHandler.php index 7a19ebe7bf3..185b6b275d1 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Language/CachingHandler.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Language/CachingHandler.php @@ -8,6 +8,7 @@ */ namespace eZ\Publish\Core\Persistence\Legacy\Content\Language; +use eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache; use eZ\Publish\SPI\Persistence\Content\Language; use eZ\Publish\SPI\Persistence\Content\Language\Handler as BaseLanguageHandler; use eZ\Publish\SPI\Persistence\Content\Language\CreateStruct; @@ -27,40 +28,20 @@ class CachingHandler implements BaseLanguageHandler /** * Language cache. * - * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\Cache + * @var \eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache */ - protected $languageCache; - - /** - * If the cache has already been initialized. - * - * @var bool - */ - protected $isCacheInitialized = false; + protected $cache; /** * Creates a caching handler around $innerHandler. * * @param \eZ\Publish\SPI\Persistence\Content\Language\Handler $innerHandler + * @param \eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache $cache */ - public function __construct(BaseLanguageHandler $innerHandler, Cache $languageCache) + public function __construct(BaseLanguageHandler $innerHandler, InMemoryCache $cache) { $this->innerHandler = $innerHandler; - $this->languageCache = $languageCache; - } - - /** - * Initializes the cache if necessary. - */ - protected function initializeCache() - { - if (false === $this->isCacheInitialized) { - $languages = $this->innerHandler->loadAll(); - foreach ($languages as $language) { - $this->languageCache->store($language); - } - $this->isCacheInitialized = true; - } + $this->cache = $cache; } /** @@ -72,9 +53,8 @@ protected function initializeCache() */ public function create(CreateStruct $struct) { - $this->initializeCache(); $language = $this->innerHandler->create($struct); - $this->languageCache->store($language); + $this->storeCache([$language]); return $language; } @@ -86,9 +66,8 @@ public function create(CreateStruct $struct) */ public function update(Language $language) { - $this->initializeCache(); $this->innerHandler->update($language); - $this->languageCache->store($language); + $this->storeCache([$language]); } /** @@ -102,9 +81,13 @@ public function update(Language $language) */ public function load($id) { - $this->initializeCache(); + $language = $this->cache->get('ez-language-' . $id); + if ($language === null) { + $language = $this->innerHandler->load($id); + $this->storeCache([$language]); + } - return $this->languageCache->getById($id); + return $language; } /** @@ -112,9 +95,24 @@ public function load($id) */ public function loadList(array $ids): iterable { - $this->initializeCache(); + $missing = []; + $languages = []; + foreach ($ids as $id) { + if ($language = $this->cache->get('ez-language-' . $id)) { + $languages[$id] = $language; + } else { + $missing[] = $id; + } + } + + if (!empty($missing)) { + $loaded = $this->innerHandler->loadList($missing); + $this->storeCache($loaded); + /** @noinspection AdditionOperationOnArraysInspection */ + $languages += $loaded; + } - return $this->languageCache->getListById($ids); + return $languages; } /** @@ -128,9 +126,13 @@ public function loadList(array $ids): iterable */ public function loadByLanguageCode($languageCode) { - $this->initializeCache(); + $language = $this->cache->get('ez-language-code-' . $languageCode); + if ($language === null) { + $language = $this->innerHandler->loadByLanguageCode($languageCode); + $this->storeCache([$language]); + } - return $this->languageCache->getByLocale($languageCode); + return $language; } /** @@ -138,9 +140,24 @@ public function loadByLanguageCode($languageCode) */ public function loadListByLanguageCodes(array $languageCodes): iterable { - $this->initializeCache(); + $missing = []; + $languages = []; + foreach ($languageCodes as $languageCode) { + if ($language = $this->cache->get('ez-language-code-' . $languageCode)) { + $languages[$languageCode] = $language; + } else { + $missing[] = $languageCode; + } + } - return $this->languageCache->getListByLocale($languageCodes); + if (!empty($missing)) { + $loaded = $this->innerHandler->loadListByLanguageCodes($missing); + $this->storeCache($loaded); + /** @noinspection AdditionOperationOnArraysInspection */ + $languages += $loaded; + } + + return $languages; } /** @@ -150,9 +167,13 @@ public function loadListByLanguageCodes(array $languageCodes): iterable */ public function loadAll() { - $this->initializeCache(); + $languages = $this->cache->get('ez-language-list'); + if ($languages === null) { + $languages = $this->innerHandler->loadAll(); + $this->storeCache($languages, 'ez-language-list'); + } - return $this->languageCache->getAll(); + return $languages; } /** @@ -162,9 +183,9 @@ public function loadAll() */ public function delete($id) { - $this->initializeCache(); $this->innerHandler->delete($id); - $this->languageCache->remove($id); + // Delete by primary key will remove the object, so we don't need to clear `ez-language-code-` here. + $this->cache->deleteMulti(['ez-language-' . $id, 'ez-language-list']); } /** @@ -172,7 +193,20 @@ public function delete($id) */ public function clearCache() { - $this->isCacheInitialized = false; - $this->languageCache->clearCache(); + $this->cache->clear(); + } + + protected function storeCache(array $languages, string $listIndex = null) + { + $this->cache->setMulti( + $languages, + static function (Language $language) { + return [ + 'ez-language-' . $language->id, + 'ez-language-code-' . $language->languageCode, + ]; + }, + $listIndex + ); } } diff --git a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Language/CacheTest.php b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Language/CacheTest.php deleted file mode 100644 index 9c96de68a82..00000000000 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Language/CacheTest.php +++ /dev/null @@ -1,187 +0,0 @@ -getCache(); - - $languageFixture = $this->getLanguageFixture(); - - $cache->store($languageFixture); - - $this->assertAttributeEquals( - array( - $languageFixture->id => $languageFixture, - ), - 'mapById', - $cache - ); - $this->assertAttributeEquals( - array( - $languageFixture->languageCode => $languageFixture, - ), - 'mapByLocale', - $cache - ); - } - - /** - * @covers \eZ\Publish\Core\Persistence\Legacy\Content\Language\Cache::remove - */ - public function testRemove() - { - $cache = $this->getCache(); - - $languageFixture = $this->getLanguageFixture(); - - $cache->store($languageFixture); - $cache->remove($languageFixture->id); - - $this->assertAttributeEquals( - array(), - 'mapById', - $cache - ); - $this->assertAttributeEquals( - array(), - 'mapByLocale', - $cache - ); - } - - /** - * @covers \eZ\Publish\Core\Persistence\Legacy\Content\Language\Cache::getById - */ - public function testGetById() - { - $cache = $this->getCache(); - - $languageFixture = $this->getLanguageFixture(); - - $cache->store($languageFixture); - - $this->assertSame( - $languageFixture, - $cache->getById($languageFixture->id) - ); - } - - /** - * @covers \eZ\Publish\Core\Persistence\Legacy\Content\Language\Cache::getById - * @expectedException \eZ\Publish\Core\Base\Exceptions\NotFoundException - */ - public function testGetByIdFailure() - { - $cache = $this->getCache(); - - $languageFixture = $this->getLanguageFixture(); - - // $cache->store( $languageFixture ); - $cache->getById($languageFixture->id); - } - - /** - * @covers \eZ\Publish\Core\Persistence\Legacy\Content\Language\Cache::getByLocale - */ - public function testGetByLocale() - { - $cache = $this->getCache(); - - $languageFixture = $this->getLanguageFixture(); - - $cache->store($languageFixture); - - $this->assertSame( - $languageFixture, - $cache->getByLocale($languageFixture->languageCode) - ); - } - - /** - * @covers \eZ\Publish\Core\Persistence\Legacy\Content\Language\Cache::getByLocale - * @expectedException \eZ\Publish\Core\Base\Exceptions\NotFoundException - */ - public function testGetByLocaleFailure() - { - $cache = $this->getCache(); - - $languageFixture = $this->getLanguageFixture(); - - // $cache->store( $languageFixture ); - $cache->getByLocale($languageFixture->languageCode); - } - - /** - * @covers \eZ\Publish\Core\Persistence\Legacy\Content\Language\Cache::getAll - */ - public function testGetAll() - { - $cache = $this->getCache(); - - $languageFixture = $this->getLanguageFixture(); - - $cache->store($languageFixture); - - $this->assertSame( - array($languageFixture->languageCode => $languageFixture), - $cache->getAll() - ); - } - - /** - * Returns the language cache to test. - * - * @return \eZ\Publish\Core\Persistence\Legacy\Content\Language\Cache - */ - protected function getCache() - { - if (!isset($this->cache)) { - $this->cache = new Cache(); - } - - return $this->cache; - } - - /** - * Returns language fixture. - * - * @return \eZ\Publish\SPI\Persistence\Content\Language - */ - protected function getLanguageFixture() - { - $langUs = new Language(); - - $langUs->id = 2; - $langUs->languageCode = 'eng-US'; - $langUs->name = 'English (American)'; - $langUs->isEnabled = true; - - return $langUs; - } -} diff --git a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Language/CachingLanguageHandlerTest.php b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Language/CachingLanguageHandlerTest.php index f6d466c9b77..42a34cfa749 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Language/CachingLanguageHandlerTest.php +++ b/eZ/Publish/Core/Persistence/Legacy/Tests/Content/Language/CachingLanguageHandlerTest.php @@ -8,13 +8,14 @@ */ namespace eZ\Publish\Core\Persistence\Legacy\Tests\Content\Language; +use eZ\Publish\API\Repository\Exceptions\NotFoundException as APINotFoundException; use eZ\Publish\Core\Persistence\Legacy\Tests\TestCase; use eZ\Publish\SPI\Persistence\Content\Language; use eZ\Publish\SPI\Persistence\Content\Language\CreateStruct as SPILanguageCreateStruct; use eZ\Publish\Core\Base\Exceptions\NotFoundException; use eZ\Publish\SPI\Persistence\Content\Language\Handler as SPILanguageHandler; use eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler; -use eZ\Publish\Core\Persistence\Legacy\Content\Language\Cache; +use eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache; /** * Test case for caching Language Handler. @@ -38,7 +39,7 @@ class CachingLanguageHandlerTest extends TestCase /** * Language cache mock. * - * @var \eZ\Publish\Core\Persistence\Legacy\Content\Language\Cache + * @var \eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache */ protected $languageCacheMock; @@ -65,7 +66,7 @@ public function testCtorPropertyLanguageCache() $this->assertAttributeSame( $this->getLanguageCacheMock(), - 'languageCache', + 'cache', $handler ); } @@ -75,8 +76,6 @@ public function testCtorPropertyLanguageCache() */ public function testCreate() { - $this->expectCacheInitialize(); - $handler = $this->getLanguageHandler(); $innerHandlerMock = $this->getInnerLanguageHandlerMock(); $cacheMock = $this->getLanguageCacheMock(); @@ -91,10 +90,9 @@ public function testCreate() ) )->will($this->returnValue($languageFixture)); - // Cache has been initialized before - $cacheMock->expects($this->at(2)) - ->method('store') - ->with($this->equalTo($languageFixture)); + $cacheMock->expects($this->once()) + ->method('setMulti') + ->with($this->equalTo([$languageFixture])); $createStruct = $this->getCreateStructFixture(); @@ -135,8 +133,6 @@ protected function getLanguageFixture() */ public function testUpdate() { - $this->expectCacheInitialize(); - $handler = $this->getLanguageHandler(); $innerHandlerMock = $this->getInnerLanguageHandlerMock(); @@ -146,12 +142,12 @@ public function testUpdate() ->method('update') ->with($this->getLanguageFixture()); - // Cache has been initialized before - $cacheMock->expects($this->at(2)) - ->method('store') - ->with($this->getLanguageFixture()); + $languageFixture = $this->getLanguageFixture(); + $cacheMock->expects($this->once()) + ->method('setMulti') + ->with($this->equalTo([$languageFixture])); - $handler->update($this->getLanguageFixture()); + $handler->update($languageFixture); } /** @@ -159,15 +155,13 @@ public function testUpdate() */ public function testLoad() { - $this->expectCacheInitialize(); - $handler = $this->getLanguageHandler(); $cacheMock = $this->getLanguageCacheMock(); $cacheMock->expects($this->once()) - ->method('getById') - ->with($this->equalTo(2)) - ->will($this->returnValue($this->getLanguageFixture())); + ->method('get') + ->with($this->equalTo('ez-language-2')) + ->willReturn($this->getLanguageFixture()); $result = $handler->load(2); @@ -179,17 +173,20 @@ public function testLoad() /** * @covers \eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler::load - * @expectedException \eZ\Publish\API\Repository\Exceptions\NotFoundException */ public function testLoadFailure() { - $this->expectCacheInitialize(); - $handler = $this->getLanguageHandler(); $cacheMock = $this->getLanguageCacheMock(); + $innerHandlerMock = $this->getInnerLanguageHandlerMock(); $cacheMock->expects($this->once()) - ->method('getById') + ->method('get') + ->with($this->equalTo('ez-language-2')) + ->willReturn(null); + + $innerHandlerMock->expects($this->once()) + ->method('load') ->with($this->equalTo(2)) ->will( $this->throwException( @@ -197,7 +194,8 @@ public function testLoadFailure() ) ); - $result = $handler->load(2); + $this->expectException(APINotFoundException::class); + $handler->load(2); } /** @@ -205,15 +203,13 @@ public function testLoadFailure() */ public function testLoadByLanguageCode() { - $this->expectCacheInitialize(); - $handler = $this->getLanguageHandler(); $cacheMock = $this->getLanguageCacheMock(); $cacheMock->expects($this->once()) - ->method('getByLocale') - ->with($this->equalTo('eng-US')) - ->will($this->returnValue($this->getLanguageFixture())); + ->method('get') + ->with($this->equalTo('ez-language-code-eng-US')) + ->willReturn($this->getLanguageFixture()); $result = $handler->loadByLanguageCode('eng-US'); @@ -225,25 +221,29 @@ public function testLoadByLanguageCode() /** * @covers \eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler::loadByLanguageCode - * @expectedException \eZ\Publish\API\Repository\Exceptions\NotFoundException */ public function testLoadByLanguageCodeFailure() { - $this->expectCacheInitialize(); - $handler = $this->getLanguageHandler(); $cacheMock = $this->getLanguageCacheMock(); + $innerHandlerMock = $this->getInnerLanguageHandlerMock(); $cacheMock->expects($this->once()) - ->method('getByLocale') + ->method('get') + ->with($this->equalTo('ez-language-code-eng-US')) + ->willReturn(null); + + $innerHandlerMock->expects($this->once()) + ->method('loadByLanguageCode') ->with($this->equalTo('eng-US')) ->will( $this->throwException( - new NotFoundException('Language', 'eng-US') + new NotFoundException('Language', 2) ) ); - $result = $handler->loadByLanguageCode('eng-US'); + $this->expectException(APINotFoundException::class); + $handler->loadByLanguageCode('eng-US'); } /** @@ -251,14 +251,13 @@ public function testLoadByLanguageCodeFailure() */ public function testLoadAll() { - $this->expectCacheInitialize(); - $handler = $this->getLanguageHandler(); $cacheMock = $this->getLanguageCacheMock(); $cacheMock->expects($this->once()) - ->method('getAll') - ->will($this->returnValue(array())); + ->method('get') + ->with($this->equalTo('ez-language-list')) + ->willReturn([]); $result = $handler->loadAll(); @@ -273,8 +272,6 @@ public function testLoadAll() */ public function testDelete() { - $this->expectCacheInitialize(); - $handler = $this->getLanguageHandler(); $cacheMock = $this->getLanguageCacheMock(); $innerHandlerMock = $this->getInnerLanguageHandlerMock(); @@ -284,8 +281,8 @@ public function testDelete() ->with($this->equalTo(2)); $cacheMock->expects($this->once()) - ->method('remove') - ->with($this->equalTo(2)); + ->method('deleteMulti') + ->with($this->equalTo(['ez-language-2', 'ez-language-list'])); $result = $handler->delete(2); } @@ -310,7 +307,7 @@ protected function getLanguageHandler() /** * Returns a mock for the inner language handler. * - * @return \eZ\Publish\SPI\Persistence\Content\Language\Handler + * @return \eZ\Publish\SPI\Persistence\Content\Language\Handler|\PHPUnit\Framework\MockObject\MockObject */ protected function getInnerLanguageHandlerMock() { @@ -322,30 +319,19 @@ protected function getInnerLanguageHandlerMock() } /** - * Returns a mock for the language cache. + * Returns a mock for the in-memory cache. * - * @return \eZ\Publish\Core\Persistence\Legacy\Content\Language\Cache + * @return \eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache|\PHPUnit\Framework\MockObject\MockObject */ protected function getLanguageCacheMock() { if (!isset($this->languageCacheMock)) { - $this->languageCacheMock = $this->createMock(Cache::class); + $this->languageCacheMock = $this->createMock(InMemoryCache::class); } return $this->languageCacheMock; } - /** - * Adds expectation for cache initialize to mocks. - */ - protected function expectCacheInitialize() - { - $innerHandlerMock = $this->getInnerLanguageHandlerMock(); - $innerHandlerMock->expects($this->once()) - ->method('loadAll') - ->will($this->returnValue($this->getLanguagesFixture())); - } - /** * Returns an array with 2 languages. * diff --git a/eZ/Publish/Core/settings/storage_engines/legacy/language.yml b/eZ/Publish/Core/settings/storage_engines/legacy/language.yml index ea76eac2ace..7d8d275f007 100644 --- a/eZ/Publish/Core/settings/storage_engines/legacy/language.yml +++ b/eZ/Publish/Core/settings/storage_engines/legacy/language.yml @@ -1,7 +1,6 @@ parameters: ezpublish.spi.persistence.legacy.language.handler.class: eZ\Publish\Core\Persistence\Legacy\Content\Language\Handler ezpublish.spi.persistence.legacy.language.handler.caching.class: eZ\Publish\Core\Persistence\Legacy\Content\Language\CachingHandler - ezpublish.persistence.legacy.language.cache.class: eZ\Publish\Core\Persistence\Legacy\Content\Language\Cache ezpublish.persistence.legacy.language.mapper.class: eZ\Publish\Core\Persistence\Legacy\Content\Language\Mapper ezpublish.persistence.legacy.language.gateway.class: eZ\Publish\Core\Persistence\Legacy\Content\Language\Gateway\DoctrineDatabase ezpublish.persistence.legacy.language.gateway.exception_conversion.class: eZ\Publish\Core\Persistence\Legacy\Content\Language\Gateway\ExceptionConversion @@ -31,15 +30,12 @@ services: - "@ezpublish.persistence.legacy.language.gateway" - "@ezpublish.persistence.legacy.language.mapper" - ezpublish.persistence.legacy.language.cache: - class: "%ezpublish.persistence.legacy.language.cache.class%" - ezpublish.spi.persistence.legacy.language.handler.caching: class: "%ezpublish.spi.persistence.legacy.language.handler.caching.class%" lazy: true arguments: - "@ezpublish.spi.persistence.legacy.language.handler.inner" - - "@ezpublish.persistence.legacy.language.cache" + - "@ezpublish.spi.persistence.cache.inmemory" ezpublish.spi.persistence.legacy.language.handler: alias: ezpublish.spi.persistence.legacy.language.handler.caching From ce8e0d61924e96baf1fcbe3038dcc3083c0e7fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20R?= Date: Mon, 4 Mar 2019 19:40:34 +0100 Subject: [PATCH 09/17] Change Legacy Storage Engine content type cache handler change to reuse InMemoryCache --- .../Content/Type/MemoryCachingHandler.php | 471 +++++++----------- .../storage_engines/legacy/content_type.yml | 1 + 2 files changed, 193 insertions(+), 279 deletions(-) diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Type/MemoryCachingHandler.php b/eZ/Publish/Core/Persistence/Legacy/Content/Type/MemoryCachingHandler.php index b4eba96fee7..cec5c642df0 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Type/MemoryCachingHandler.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Type/MemoryCachingHandler.php @@ -8,14 +8,15 @@ */ namespace eZ\Publish\Core\Persistence\Legacy\Content\Type; +use eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache; use eZ\Publish\SPI\Persistence\Content\Type; use eZ\Publish\SPI\Persistence\Content\Type\Handler as BaseContentTypeHandler; use eZ\Publish\SPI\Persistence\Content\Type\CreateStruct; use eZ\Publish\SPI\Persistence\Content\Type\UpdateStruct; use eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition; +use eZ\Publish\SPI\Persistence\Content\Type\Group; use eZ\Publish\SPI\Persistence\Content\Type\Group\CreateStruct as GroupCreateStruct; use eZ\Publish\SPI\Persistence\Content\Type\Group\UpdateStruct as GroupUpdateStruct; -use eZ\Publish\Core\Persistence\Legacy\Exception; class MemoryCachingHandler implements BaseContentTypeHandler { @@ -27,94 +28,70 @@ class MemoryCachingHandler implements BaseContentTypeHandler protected $innerHandler; /** - * Local in-memory cache for groups in one single request. + * Type cache. * - * @var \eZ\Publish\SPI\Persistence\Content\Type\Group[] + * @var \eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache */ - protected $groups = []; - - /** - * Local in-memory cache for content types in one single request. - * - * @var \eZ\Publish\SPI\Persistence\Content\Type[][] - */ - protected $contentTypes = []; - - /** - * Local in-memory cache for field definitions in one single request. - * - * @var array - */ - protected $fieldDefinitions; - - /** - * Local in-memory cache for searchable field map in one single request. - * - * @var array - */ - protected $searchableFieldMap = null; + protected $cache; /** * Creates a new content type handler. * * @param \eZ\Publish\SPI\Persistence\Content\Type\Handler $handler + * @param \eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache $cache */ - public function __construct(BaseContentTypeHandler $handler) + public function __construct(BaseContentTypeHandler $handler, InMemoryCache $cache) { $this->innerHandler = $handler; + $this->cache = $cache; } /** - * @param \eZ\Publish\SPI\Persistence\Content\Type\Group\CreateStruct $createStruct - * - * @return \eZ\Publish\SPI\Persistence\Content\Type\Group + * {@inheritdoc} */ public function createGroup(GroupCreateStruct $createStruct) { - $this->clearCache(); + $group = $this->innerHandler->createGroup($createStruct); + $this->storeGroupCache([$group]); + $this->cache->deleteMulti(['ez-content-type-group-list']); - return $this->innerHandler->createGroup($createStruct); + return $group; } /** - * @param \eZ\Publish\SPI\Persistence\Content\Type\Group\UpdateStruct $struct - * - * @return \eZ\Publish\SPI\Persistence\Content\Type\Group + * {@inheritdoc} */ public function updateGroup(GroupUpdateStruct $struct) { - $this->clearCache(); + $group = $this->innerHandler->updateGroup($struct); + $this->storeGroupCache([$group]); + $this->cache->deleteMulti(['ez-content-type-group-list']); - return $this->innerHandler->updateGroup($struct); + return $group; } /** - * @param mixed $groupId - * - * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException If type group contains types - * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException If type group with id is not found + * {@inheritdoc} */ public function deleteGroup($groupId) { - $this->clearCache(); - - return $this->innerHandler->deleteGroup($groupId); + $this->innerHandler->deleteGroup($groupId); + // Delete by primary key will remove the object, so we don't need to clear `-by-identifier` variant here. + $this->cache->deleteMulti(['ez-content-type-group-' . $groupId, 'ez-content-type-group-list']); } /** - * @param mixed $groupId - * - * @return \eZ\Publish\SPI\Persistence\Content\Type\Group - * - * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException If type group with $groupId is not found + * {@inheritdoc} */ public function loadGroup($groupId) { - if (isset($this->groups[$groupId])) { - return $this->groups[$groupId]; + $group = $this->cache->get('ez-content-type-group-' . $groupId); + if ($group === null) { + $group = $this->innerHandler->loadGroup($groupId); + $this->storeGroupCache([$group]); } - return $this->groups[$groupId] = $this->innerHandler->loadGroup($groupId); + return $group; } /** @@ -124,306 +101,228 @@ public function loadGroups(array $groupIds) { $groups = $missingIds = []; foreach ($groupIds as $groupId) { - if (isset($this->groups[$groupId])) { - $groups[$groupId] = $this->groups[$groupId]; + if ($group = $this->cache->get('ez-content-type-group-' . $groupId)) { + $groups[$groupId] = $group; } else { $missingIds[] = $groupId; } } if (!empty($missingIds)) { - $missing = $this->innerHandler->loadGroups($missingIds); - $groups += $missing; - $this->groups += $missing; + $loaded = $this->innerHandler->loadGroups($missingIds); + $this->storeGroupCache($loaded); + /** @noinspection AdditionOperationOnArraysInspection */ + $groups += $loaded; } return $groups; } /** - * @param string $identifier - * - * @return \eZ\Publish\SPI\Persistence\Content\Type\Group - * - * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException If type group with $identifier is not found + * {@inheritdoc} */ public function loadGroupByIdentifier($identifier) { - if (isset($this->groups[$identifier])) { - return $this->groups[$identifier]; + $group = $this->cache->get('ez-content-type-group-' . $identifier . '-by-identifier'); + if ($group === null) { + $group = $this->innerHandler->loadGroupByIdentifier($identifier); + $this->storeGroupCache([$group]); } - return $this->groups[$identifier] = $this->innerHandler->loadGroupByIdentifier($identifier); + return $group; } /** - * @return \eZ\Publish\SPI\Persistence\Content\Type\Group[] + * {@inheritdoc} */ public function loadAllGroups() { - return $this->innerHandler->loadAllGroups(); + $groups = $this->cache->get('ez-content-type-group-list'); + if ($groups === null) { + $groups = $this->innerHandler->loadAllGroups(); + $this->storeGroupCache($groups, 'ez-content-type-group-list'); + } + + return $groups; } /** - * @param mixed $groupId - * @param int $status - * - * @return Type[] + * {@inheritdoc} */ - public function loadContentTypes($groupId, $status = 0) + public function loadContentTypes($groupId, $status = Type::STATUS_DEFINED) { - return $this->innerHandler->loadContentTypes($groupId, $status); + if ($status !== Type::STATUS_DEFINED) { + return $this->innerHandler->loadContentTypes($groupId, $status); + } + + $types = $this->cache->get('ez-content-type-list-by-group-' . $groupId); + if ($types === null) { + $types = $this->innerHandler->loadContentTypes($groupId, $status); + $this->storeTypeCache($types, 'ez-content-type-list-by-group-' . $groupId); + } + + return $types; } + /** + * {@inheritdoc} + */ public function loadContentTypeList(array $contentTypeIds): array { $contentTypes = $missingIds = []; foreach ($contentTypeIds as $contentTypeId) { - if (isset($this->contentTypes[$contentTypeId][Type::STATUS_DEFINED])) { - $contentTypes[$contentTypeId] = $this->contentTypes[$contentTypeId][Type::STATUS_DEFINED]; + if ($contentType = $this->cache->get('ez-content-type-' . $contentTypeId . '-' . Type::STATUS_DEFINED)) { + $contentTypes[$contentTypeId] = $contentType; } else { $missingIds[] = $contentTypeId; } } if (!empty($missingIds)) { - foreach ($this->innerHandler->loadContentTypeList($missingIds) as $id => $contentType) { - $this->contentTypes[$id][Type::STATUS_DEFINED] = $contentTypes[$id] = $contentType; - } + $loaded = $this->innerHandler->loadContentTypeList($missingIds); + $this->storeTypeCache($loaded); + /** @noinspection AdditionOperationOnArraysInspection */ + $contentTypes += $loaded; } return $contentTypes; } /** - * @param int $contentTypeId - * @param int $status - * - * @return \eZ\Publish\SPI\Persistence\Content\Type + * {@inheritdoc} */ public function load($contentTypeId, $status = Type::STATUS_DEFINED) { - if (isset($this->contentTypes[$contentTypeId][$status])) { - return $this->contentTypes[$contentTypeId][$status]; + $contentType = $this->cache->get('ez-content-type-' . $contentTypeId . '-' . $status); + if ($contentType === null) { + $contentType = $this->innerHandler->load($contentTypeId, $status); + $this->storeTypeCache([$contentType]); } - return $this->contentTypes[$contentTypeId][$status] = - $this->innerHandler->load($contentTypeId, $status); + return $contentType; } /** - * Load a (defined) content type by identifier. - * - * @param string $identifier - * - * @return \eZ\Publish\SPI\Persistence\Content\Type - * - * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException If defined type is not found + * {@inheritdoc} */ public function loadByIdentifier($identifier) { - foreach ($this->contentTypes as $item) { - if (!isset($item[Type::STATUS_DEFINED])) { - continue; - } - - $contentType = $item[Type::STATUS_DEFINED]; - if ($contentType->identifier === $identifier) { - return $contentType; - } + $contentType = $this->cache->get('ez-content-type-' . $identifier . '-by-identifier'); + if ($contentType === null) { + $contentType = $this->innerHandler->loadByIdentifier($identifier); + $this->storeTypeCache([$contentType]); } - $contentType = $this->innerHandler->loadByIdentifier($identifier); - $this->contentTypes[$contentType->id][$contentType->status] = $contentType; - return $contentType; } /** - * Load a (defined) content type by remote id. - * - * @param mixed $remoteId - * - * @return \eZ\Publish\SPI\Persistence\Content\Type - * - * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException If defined type is not found + * {@inheritdoc} */ public function loadByRemoteId($remoteId) { - foreach ($this->contentTypes as $item) { - if (!isset($item[Type::STATUS_DEFINED])) { - continue; - } - - $contentType = $item[Type::STATUS_DEFINED]; - if ($contentType->remoteId === $remoteId) { - return $contentType; - } + $contentType = $this->cache->get('ez-content-type-' . $remoteId . '-by-remote'); + if ($contentType === null) { + $contentType = $this->innerHandler->loadByRemoteId($remoteId); + $this->storeTypeCache([$contentType]); } - $contentType = $this->innerHandler->loadByRemoteId($remoteId); - $this->contentTypes[$contentType->id][$contentType->status] = $contentType; - return $contentType; } /** - * Loads a single Type from $rows. - * - * @param array $rows - * @param mixed $typeIdentifier - * @param int $status - * - * @return \eZ\Publish\SPI\Persistence\Content\Type - */ - protected function loadFromRows(array $rows, $typeIdentifier, $status) - { - $types = $this->mapper->extractTypesFromRows($rows); - if (!isset($types[0])) { - throw new Exception\TypeNotFound($typeIdentifier, $status); - } - - return $types[0]; - } - - /** - * @param \eZ\Publish\SPI\Persistence\Content\Type\CreateStruct $createStruct - * - * @return \eZ\Publish\SPI\Persistence\Content\Type + * {@inheritdoc} */ public function create(CreateStruct $createStruct) { - $this->clearCache(); + $contentType = $this->innerHandler->create($createStruct); + // Don't store as FieldTypeConstraints is not setup fully here from Legacy SE side + $this->deleteTypeCache($contentType->id, $contentType->status); - return $this->innerHandler->create($createStruct); + return $contentType; } /** - * @param mixed $typeId - * @param int $status - * @param \eZ\Publish\SPI\Persistence\Content\Type\UpdateStruct $contentType - * - * @return Type + * {@inheritdoc} */ public function update($typeId, $status, UpdateStruct $contentType) { - $this->clearCache(); + $contentType = $this->innerHandler->update($typeId, $status, $contentType); + $this->storeTypeCache([$contentType]); - return $this->innerHandler->update($typeId, $status, $contentType); + return $contentType; } /** - * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException If type is defined and still has content - * - * @param mixed $contentTypeId - * @param int $status - * - * @return bool + * {@inheritdoc} */ public function delete($contentTypeId, $status) { - $this->clearCache(); - - return $this->innerHandler->delete($contentTypeId, $status); + $this->innerHandler->delete($contentTypeId, $status); + $this->deleteTypeCache($contentTypeId, $status); } /** - * Creates a draft of existing defined content type. - * - * Updates modified date, sets $modifierId and status to Type::STATUS_DRAFT on the new returned draft. - * - * @param mixed $modifierId - * @param mixed $contentTypeId - * - * @return \eZ\Publish\SPI\Persistence\Content\Type - * - * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException If type with defined status is not found + * {@inheritdoc} */ public function createDraft($modifierId, $contentTypeId) { - $this->clearCache(); + $contentType = $this->innerHandler->createDraft($modifierId, $contentTypeId); + // Don't store as FieldTypeConstraints is not setup fully here from Legacy SE side + $this->deleteTypeCache($contentType->id, $contentType->status); - return $this->innerHandler->createDraft($modifierId, $contentTypeId); + return $contentType; } /** - * @param mixed $userId - * @param mixed $contentTypeId - * @param int $status One of Type::STATUS_DEFINED|Type::STATUS_DRAFT|Type::STATUS_MODIFIED - * - * @return Type + * {@inheritdoc} */ public function copy($userId, $contentTypeId, $status) { - $this->clearCache(); + $contentType = $this->innerHandler->copy($userId, $contentTypeId, $status); + $this->storeTypeCache([$contentType]); - return $this->innerHandler->copy($userId, $contentTypeId, $status); + return $contentType; } /** - * Unlink a content type group from a content type. - * - * @param mixed $groupId - * @param mixed $contentTypeId - * @param int $status - * - * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException If group or type with provided status is not found - * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException If $groupId is last group on $contentTypeId or - * not a group assigned to type - * - * @todo Add throws for NotFound and BadState when group is not assigned to type + * {@inheritdoc} */ public function unlink($groupId, $contentTypeId, $status) { - $this->clearCache(); + $keys = ['ez-content-type-' . $contentTypeId . '-' . $status]; + if ($status === Type::STATUS_DEFINED) { + $keys[] = 'ez-content-type-list-by-group-' . $groupId; + } + $this->cache->deleteMulti($keys); return $this->innerHandler->unlink($groupId, $contentTypeId, $status); } /** - * Link a content type group with a content type. - * - * @param mixed $groupId - * @param mixed $contentTypeId - * - * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException If group or type with provided status is not found - * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException If type is already part of group - * - * @todo Above throws are not implemented + * {@inheritdoc} */ public function link($groupId, $contentTypeId, $status) { - $this->clearCache(); + $keys = ['ez-content-type-' . $contentTypeId . '-' . $status]; + if ($status === Type::STATUS_DEFINED) { + $keys[] = 'ez-content-type-list-by-group-' . $groupId; + } + $this->cache->deleteMulti($keys); return $this->innerHandler->link($groupId, $contentTypeId, $status); } /** - * Returns field definition for the given field definition id. - * - * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException If field definition is not found - * - * @param mixed $id - * @param int $status One of Type::STATUS_DEFINED|Type::STATUS_DRAFT|Type::STATUS_MODIFIED - * - * @return \eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition + * {@inheritdoc} */ public function getFieldDefinition($id, $status) { - if (isset($this->fieldDefinitions[$id][$status])) { - return $this->fieldDefinitions[$id][$status]; - } - - return $this->fieldDefinitions[$id][$status] = - $this->innerHandler->getFieldDefinition($id, $status); + return $this->innerHandler->getFieldDefinition($id, $status); } /** - * Counts the number of Content instances of the ContentType identified by given $contentTypeId. - * - * @param mixed $contentTypeId - * - * @return int + * {@inheritdoc} */ public function getContentCount($contentTypeId) { @@ -431,71 +330,37 @@ public function getContentCount($contentTypeId) } /** - * Adds a new field definition to an existing Type. - * - * This method creates a new status of the Type with the $fieldDefinition - * added. It does not update existing content objects depending on the - * field (default) values. - * - * @param mixed $contentTypeId - * @param int $status One of Type::STATUS_DEFINED|Type::STATUS_DRAFT|Type::STATUS_MODIFIED - * @param \eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition $fieldDefinition + * {@inheritdoc} */ public function addFieldDefinition($contentTypeId, $status, FieldDefinition $fieldDefinition) { - $this->clearCache(); + $this->deleteTypeCache($contentTypeId, $status); return $this->innerHandler->addFieldDefinition($contentTypeId, $status, $fieldDefinition); } /** - * Removes a field definition from an existing Type. - * - * This method creates a new status of the Type with the field definition - * referred to by $fieldDefinitionId removed. It does not update existing - * content objects depending on the field (default) values. - * - * @param mixed $contentTypeId - * @param mixed $fieldDefinitionId - * - * @return bool + * {@inheritdoc} */ public function removeFieldDefinition($contentTypeId, $status, $fieldDefinitionId) { - $this->clearCache(); + $this->deleteTypeCache($contentTypeId, $status); return $this->innerHandler->removeFieldDefinition($contentTypeId, $status, $fieldDefinitionId); } /** - * This method updates the given $fieldDefinition on a Type. - * - * This method creates a new status of the Type with the updated - * $fieldDefinition. It does not update existing content objects depending - * on the - * field (default) values. - * - * @param mixed $contentTypeId - * @param \eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition $fieldDefinition + * {@inheritdoc} */ public function updateFieldDefinition($contentTypeId, $status, FieldDefinition $fieldDefinition) { - $this->clearCache(); + $this->deleteTypeCache($contentTypeId, $status); return $this->innerHandler->updateFieldDefinition($contentTypeId, $status, $fieldDefinition); } /** - * Update content objects. - * - * Updates content objects, depending on the changed field definitions. - * - * A content type has a state which tells if its content objects yet have - * been adapted. - * - * Flags the content type as updated. - * - * @param mixed $contentTypeId + * {@inheritdoc} */ public function publish($contentTypeId) { @@ -505,36 +370,84 @@ public function publish($contentTypeId) } /** - * @see \eZ\Publish\SPI\Persistence\Content\Type\Handler::getSearchableFieldMap + * {@inheritdoc} */ public function getSearchableFieldMap() { - if ($this->searchableFieldMap !== null) { - return $this->searchableFieldMap; + $map = $this->cache->get('ez-content-type-field-map'); + if ($map === null) { + $map = $this->innerHandler->getSearchableFieldMap(); + $this->cache->setMulti( + $map, + static function () { return []; }, + 'ez-content-type-field-map' + ); } - return $this->searchableFieldMap = $this->innerHandler->getSearchableFieldMap(); + return $map; } /** - * Clear internal caches. + * {@inheritdoc} */ - public function clearCache() + public function removeContentTypeTranslation(int $contentTypeId, string $languageCode): Type { - $this->groups = $this->contentTypes = $this->fieldDefinitions = array(); - $this->searchableFieldMap = null; + $this->clearCache(); + + return $this->innerHandler->removeContentTypeTranslation($contentTypeId, $languageCode); } /** - * @param int $contentTypeId - * @param string $languageCode - * - * @return \eZ\Publish\SPI\Persistence\Content\Type + * Clear internal caches. */ - public function removeContentTypeTranslation(int $contentTypeId, string $languageCode): Type + public function clearCache() { - $this->clearCache(); + $this->cache->clear(); + } - return $this->innerHandler->removeContentTypeTranslation($contentTypeId, $languageCode); + protected function deleteTypeCache(int $contentTypeId, int $status = Type::STATUS_DEFINED): void + { + if ($status !== Type::STATUS_DEFINED) { + // Delete by primary key will remove the object, so we don't need to clear other variants here. + $this->cache->deleteMulti(['ez-content-type-' . $contentTypeId . '-' . $status, 'ez-content-type-field-map']); + } else { + // We don't know group id in order to clear relevant "ez-content-type-list-by-group-$groupId". + $this->cache->clear(); + } + } + + protected function storeTypeCache(array $types, string $listIndex = null): void + { + $this->cache->setMulti( + $types, + static function (Type $type) { + if ($type->status !== Type::STATUS_DEFINED) { + return ['ez-content-type-' . $type->id . '-' . $type->status]; + } + + return [ + 'ez-content-type-' . $type->id . '-' . $type->status, + 'ez-content-type-' . $type->identifier . '-by-identifier', + 'ez-content-type-' . $type->remoteId . '-by-remote', + ]; + }, + $listIndex + ); + + $this->cache->deleteMulti(['ez-content-type-field-map']); + } + + protected function storeGroupCache(array $groups, string $listIndex = null): void + { + $this->cache->setMulti( + $groups, + static function (Group $group) { + return [ + 'ez-content-type-group-' . $group->id, + 'ez-content-type-group-' . $group->identifier . '-by-identifier', + ]; + }, + $listIndex + ); } } diff --git a/eZ/Publish/Core/settings/storage_engines/legacy/content_type.yml b/eZ/Publish/Core/settings/storage_engines/legacy/content_type.yml index e82589706e5..07f6df5a790 100644 --- a/eZ/Publish/Core/settings/storage_engines/legacy/content_type.yml +++ b/eZ/Publish/Core/settings/storage_engines/legacy/content_type.yml @@ -77,6 +77,7 @@ services: lazy: true arguments: - "@ezpublish.spi.persistence.legacy.content_type.handler.inner" + - "@ezpublish.spi.persistence.cache.inmemory" ezpublish.spi.persistence.legacy.content_type.handler: alias: ezpublish.spi.persistence.legacy.content_type.handler.caching From 924be6348674d634f9cf18dcd953e388c2637cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20R?= Date: Tue, 5 Mar 2019 21:43:01 +0100 Subject: [PATCH 10/17] Adapt UserHandler to use InMemory cache --- .../Cache/Tests/AbstractBaseHandlerTest.php | 2 +- .../Cache/Tests/UserHandlerTest.php | 18 +- .../Core/Persistence/Cache/UserHandler.php | 376 +++++++++--------- .../Core/settings/storage_engines/cache.yml | 2 +- 4 files changed, 210 insertions(+), 188 deletions(-) diff --git a/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php b/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php index 77684498bd2..65a70c2a517 100644 --- a/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php +++ b/eZ/Publish/Core/Persistence/Cache/Tests/AbstractBaseHandlerTest.php @@ -84,7 +84,7 @@ final protected function setUp() new CacheContentHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), new CacheContentLanguageHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock, $this->inMemoryMock), new CacheContentTypeHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock, $this->inMemoryMock), - new CacheUserHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), + new CacheUserHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock, $this->inMemoryMock), new CacheTransactionHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock, $this->inMemoryMock), new CacheTrashHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), new CacheUrlAliasHandler($this->cacheMock, $this->persistenceHandlerMock, $this->loggerMock), diff --git a/eZ/Publish/Core/Persistence/Cache/Tests/UserHandlerTest.php b/eZ/Publish/Core/Persistence/Cache/Tests/UserHandlerTest.php index 06810774b56..48e53f74563 100644 --- a/eZ/Publish/Core/Persistence/Cache/Tests/UserHandlerTest.php +++ b/eZ/Publish/Core/Persistence/Cache/Tests/UserHandlerTest.php @@ -21,7 +21,7 @@ /** * Test case for Persistence\Cache\UserHandler. */ -class UserHandlerTest extends AbstractCacheHandlerTest +class UserHandlerTest extends AbstractInMemoryCacheHandlerTest { public function getHandlerMethodName(): string { @@ -35,16 +35,20 @@ public function getHandlerClassName(): string public function providerForUnCachedMethods(): array { - $user = new User(['id' => 14]); + $user = new User(['id' => 14, 'login' => 'otto', 'email' => 'otto@ez.no']); $policy = new Policy(['id' => 13, 'roleId' => 9]); - $userToken = new User\UserTokenUpdateStruct(['userId' => 14]); + $userToken = new User\UserTokenUpdateStruct(['userId' => 14, 'hashKey' => '4irj8t43r']); - // string $method, array $arguments, array? $tags, string? $key + // string $method, array $arguments, array? $tags, array? $key return [ - ['create', [$user], ['content-fields-14']], + ['create', [$user], ['content-fields-14'], [ + 'ez-user-14', + 'ez-user-' . str_replace('@', '§', $user->login) . '-by-login', + 'ez-user-' . str_replace('@', '§', $user->email) . '-by-email', + ]], ['update', [$user], ['content-fields-14', 'user-14']], - ['updateUserToken', [$userToken], ['content-fields-14', 'user-14']], - ['expireUserToken', [14]], + ['updateUserToken', [$userToken], ['user-14-account-key'], ['ez-user-4irj8t43r-by-account-key']], + ['expireUserToken', ['4irj8t43r'], null, ['ez-user-4irj8t43r-by-account-key']], ['delete', [14], ['content-fields-14', 'user-14']], ['createRole', [new RoleCreateStruct()]], ['createRoleDraft', [new RoleCreateStruct()]], diff --git a/eZ/Publish/Core/Persistence/Cache/UserHandler.php b/eZ/Publish/Core/Persistence/Cache/UserHandler.php index 84577812574..662aa5e7012 100644 --- a/eZ/Publish/Core/Persistence/Cache/UserHandler.php +++ b/eZ/Publish/Core/Persistence/Cache/UserHandler.php @@ -8,6 +8,8 @@ */ namespace eZ\Publish\Core\Persistence\Cache; +use eZ\Publish\Core\Persistence\Cache\InMemory\InMemoryCache; +use eZ\Publish\SPI\Persistence\Handler as PersistenceHandler; use eZ\Publish\SPI\Persistence\User\UserTokenUpdateStruct; use eZ\Publish\SPI\Persistence\User\Handler as UserHandlerInterface; use eZ\Publish\SPI\Persistence\User; @@ -16,12 +18,72 @@ use eZ\Publish\SPI\Persistence\User\RoleCreateStruct; use eZ\Publish\SPI\Persistence\User\RoleUpdateStruct; use eZ\Publish\SPI\Persistence\User\Policy; +use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface; /** * Cache handler for user module. */ -class UserHandler extends AbstractHandler implements UserHandlerInterface +class UserHandler extends AbstractInMemoryHandler implements UserHandlerInterface { + /** @var callable */ + private $getUserTags; + + /** @var callable */ + private $getUserKeys; + + /** @var callable */ + private $getRoleTags; + + /** @var callable */ + private $getRoleKeys; + + /** @var callable */ + private $getRoleAssignmentTags; + + /** @var callable */ + private $getRoleAssignmentKeys; + + public function __construct( + TagAwareAdapterInterface $cache, + PersistenceHandler $persistenceHandler, + PersistenceLogger $logger, + InMemoryCache $inMemory + ) { + parent::__construct($cache, $persistenceHandler, $logger, $inMemory); + + $this->getUserTags = static function (User $user) { + return ['content-' . $user->id, 'user-' . $user->id]; + }; + $this->getUserKeys = static function (User $user) { + return [ + 'ez-user-' . $user->id, + 'ez-user-' . \str_replace('@', '§', $user->login) . '-by-login', + //'ez-user-' . $hash . '-by-account-key', + ]; + }; + $this->getRoleTags = static function (Role $role) { + return ['role-' . $role->id]; + }; + $this->getRoleKeys = static function (Role $role) { + return [ + 'ez-role-' . $role->id, + 'ez-role-' . $role->identifier . '-by-identifier', + ]; + }; + $this->getRoleAssignmentTags = static function (RoleAssignment $roleAssignment) { + return [ + 'role-assignment-' . $roleAssignment->id, + 'role-assignment-group-list-' . $roleAssignment->contentId, + 'role-assignment-role-list-' . $roleAssignment->roleId, + ]; + }; + $this->getRoleAssignmentKeys = static function (RoleAssignment $roleAssignment) { + return [ + 'ez-role-assignment-' . $roleAssignment->id, + ]; + }; + } + /** * {@inheritdoc} */ @@ -31,8 +93,9 @@ public function create(User $user) $return = $this->persistenceHandler->userHandler()->create($user); // Clear corresponding content cache as creation of the User changes it's external data - $this->cache->invalidateTags(['content-fields-' . $user->id]); - $this->cache->deleteItems([ + $this->invalidateCache(['content-fields-' . $user->id]); + $this->deleteCache([ + 'ez-user-' . $user->id, 'ez-user-' . str_replace('@', '§', $user->login) . '-by-login', 'ez-user-' . str_replace('@', '§', $user->email) . '-by-email', ]); @@ -45,19 +108,15 @@ public function create(User $user) */ public function load($userId) { - $cacheItem = $this->cache->getItem("ez-user-${userId}"); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('user' => $userId)); - $user = $this->persistenceHandler->userHandler()->load($userId); - - $cacheItem->set($user); - $cacheItem->tag(['content-' . $user->id, 'user-' . $user->id]); - $this->cache->save($cacheItem); - - return $user; + return $this->getCacheValue( + $userId, + 'ez-user-', + function ($userId) { + return $this->persistenceHandler->userHandler()->load($userId); + }, + $this->getUserTags, + $this->getUserKeys + ); } /** @@ -65,19 +124,16 @@ public function load($userId) */ public function loadByLogin($login) { - $cacheItem = $this->cache->getItem('ez-user-' . str_replace('@', '§', $login) . '-by-login'); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('user' => $login)); - $user = $this->persistenceHandler->userHandler()->loadByLogin($login); - - $cacheItem->set($user); - $cacheItem->tag(['content-' . $user->id, 'user-' . $user->id]); - $this->cache->save($cacheItem); - - return $user; + return $this->getCacheValue( + str_replace('@', '§', $login), + 'ez-user-', + function ($escapedLogin) use ($login) { + return $this->persistenceHandler->userHandler()->loadByLogin($login); + }, + $this->getUserTags, + $this->getUserKeys, + '-by-login' + ); } /** @@ -85,24 +141,15 @@ public function loadByLogin($login) */ public function loadByEmail($email) { - $cacheItem = $this->cache->getItem('ez-user-' . str_replace('@', '§', $email) . '-by-email'); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('email' => $email)); - $users = $this->persistenceHandler->userHandler()->loadByEmail($email); - - $cacheItem->set($users); - $cacheTags = []; - foreach ($users as $user) { - $cacheTags[] = 'content-' . $user->id; - $cacheTags[] = 'user-' . $user->id; - } - $cacheItem->tag($cacheTags); - $this->cache->save($cacheItem); - - return $users; + // As load by email can return several items we threat it like a list here. + return $this->getListCacheValue( + 'ez-user-' . str_replace('@', '§', $email) . '-by-email', + function () use ($email) { + return $this->persistenceHandler->userHandler()->loadByEmail($email); + }, + $this->getUserTags, + $this->getUserKeys + ); } /** @@ -110,19 +157,30 @@ public function loadByEmail($email) */ public function loadUserByToken($hash) { - $cacheItem = $this->cache->getItem('ez-user-' . $hash . '-by-account-key'); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('hash' => $hash)); - $user = $this->persistenceHandler->userHandler()->loadUserByToken($hash); - - $cacheItem->set($user); - $cacheItem->tag(['content-' . $user->id, 'user-' . $user->id]); - $this->cache->save($cacheItem); - - return $user; + $getUserKeysFn = $this->getUserKeys; + $getUserTagsFn = $this->getUserTags; + + return $this->getCacheValue( + $hash, + 'ez-user-', + function ($hash) { + return $this->persistenceHandler->userHandler()->loadUserByToken($hash); + }, + static function (User $user) use ($getUserTagsFn) { + $tags = $getUserTagsFn($user); + // See updateUserToken() + $tags[] = 'user-' . $user->id . '-account-key'; + + return $tags; + }, + static function (User $user) use ($hash, $getUserKeysFn) { + $keys = $getUserKeysFn($user); + $keys[] = 'ez-user-' . $hash . '-by-account-key'; + + return $keys; + }, + '-by-account-key' + ); } /** @@ -134,7 +192,7 @@ public function update(User $user) $return = $this->persistenceHandler->userHandler()->update($user); // Clear corresponding content cache as update of the User changes it's external data - $this->cache->invalidateTags(['content-fields-' . $user->id, 'user-' . $user->id]); + $this->invalidateCache(['content-fields-' . $user->id, 'user-' . $user->id]); return $return; } @@ -147,8 +205,9 @@ public function updateUserToken(UserTokenUpdateStruct $userTokenUpdateStruct) $this->logger->logCall(__METHOD__, array('struct' => $userTokenUpdateStruct)); $return = $this->persistenceHandler->userHandler()->updateUserToken($userTokenUpdateStruct); - // Clear corresponding content cache as update of the User changes it's external data - $this->cache->invalidateTags(['content-fields-' . $userTokenUpdateStruct->userId, 'user-' . $userTokenUpdateStruct->userId]); + // As we 1. don't know original hash, and 2. hash is not guaranteed to be unique, we do it like this for now + $this->invalidateCache(['user-' . $userTokenUpdateStruct->userId . '-account-key']); + $this->deleteCache(['ez-user-' . $userTokenUpdateStruct->hashKey . '-by-account-key']); return $return; } @@ -160,6 +219,7 @@ public function expireUserToken($hash) { $this->logger->logCall(__METHOD__, array('hash' => $hash)); $return = $this->persistenceHandler->userHandler()->expireUserToken($hash); + $this->deleteCache(['ez-user-' . $hash . '-by-account-key']); return $return; } @@ -173,7 +233,7 @@ public function delete($userId) $return = $this->persistenceHandler->userHandler()->delete($userId); // user id == content id == group id - $this->cache->invalidateTags(['content-fields-' . $userId, 'user-' . $userId]); + $this->invalidateCache(['content-fields-' . $userId, 'user-' . $userId]); return $return; } @@ -209,19 +269,15 @@ public function loadRole($roleId, $status = Role::STATUS_DEFINED) return $this->persistenceHandler->userHandler()->loadRole($roleId, $status); } - $cacheItem = $this->cache->getItem("ez-role-${roleId}"); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('role' => $roleId)); - $role = $this->persistenceHandler->userHandler()->loadRole($roleId, $status); - - $cacheItem->set($role); - $cacheItem->tag(['role-' . $role->id]); - $this->cache->save($cacheItem); - - return $role; + return $this->getCacheValue( + $roleId, + 'ez-role-', + function ($roleId) { + return $this->persistenceHandler->userHandler()->loadRole($roleId); + }, + $this->getRoleTags, + $this->getRoleKeys + ); } /** @@ -235,19 +291,16 @@ public function loadRoleByIdentifier($identifier, $status = Role::STATUS_DEFINED return $this->persistenceHandler->userHandler()->loadRoleByIdentifier($identifier, $status); } - $cacheItem = $this->cache->getItem("ez-role-${identifier}-by-identifier"); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('role' => $identifier)); - $role = $this->persistenceHandler->userHandler()->loadRoleByIdentifier($identifier, $status); - - $cacheItem->set($role); - $cacheItem->tag(['role-' . $role->id]); - $this->cache->save($cacheItem); - - return $role; + return $this->getCacheValue( + $identifier, + 'ez-role-', + function ($identifier) { + return $this->persistenceHandler->userHandler()->loadRoleByIdentifier($identifier); + }, + $this->getRoleTags, + $this->getRoleKeys, + '-by-identifier' + ); } /** @@ -275,19 +328,15 @@ public function loadRoles() */ public function loadRoleAssignment($roleAssignmentId) { - $cacheItem = $this->cache->getItem("ez-role-assignment-${roleAssignmentId}"); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('assignment' => $roleAssignmentId)); - $roleAssignment = $this->persistenceHandler->userHandler()->loadRoleAssignment($roleAssignmentId); - - $cacheItem->set($roleAssignment); - $cacheItem->tag($this->getCacheTagsForRoleAssignment($roleAssignment)); - $this->cache->save($cacheItem); - - return $roleAssignment; + return $this->getCacheValue( + $roleAssignmentId, + 'ez-role-assignment-', + function ($roleAssignmentId) { + return $this->persistenceHandler->userHandler()->loadRoleAssignment($roleAssignmentId); + }, + $this->getRoleAssignmentTags, + $this->getRoleAssignmentKeys + ); } /** @@ -295,26 +344,18 @@ public function loadRoleAssignment($roleAssignmentId) */ public function loadRoleAssignmentsByRoleId($roleId) { - $cacheItem = $this->cache->getItem("ez-role-assignment-${roleId}-by-role"); - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('role' => $roleId)); - $roleAssignments = $this->persistenceHandler->userHandler()->loadRoleAssignmentsByRoleId($roleId); - - $cacheItem->set($roleAssignments); - $cacheTags = [ - 'role-assignment-role-list-' . $roleId, - 'role-' . $roleId, /* Role update (policies) changes role assignment id */ - ]; - foreach ($roleAssignments as $roleAssignment) { - $cacheTags = $this->getCacheTagsForRoleAssignment($roleAssignment, $cacheTags); - } - $cacheItem->tag($cacheTags); - $this->cache->save($cacheItem); - - return $roleAssignments; + return $this->getListCacheValue( + "ez-role-assignment-${roleId}-by-role", + function () use ($roleId) { + return $this->persistenceHandler->userHandler()->loadRoleAssignmentsByRoleId($roleId); + }, + $this->getRoleAssignmentTags, + $this->getRoleAssignmentKeys, + [ + 'role-' . $roleId, /* Role update (policies) changes role assignment id */ + ], + [$roleId] + ); } /** @@ -322,40 +363,34 @@ public function loadRoleAssignmentsByRoleId($roleId) */ public function loadRoleAssignmentsByGroupId($groupId, $inherit = false) { + $innerHandler = $this->persistenceHandler; if ($inherit) { - $cacheItem = $this->cache->getItem("ez-role-assignment-${groupId}-by-group-inherited"); + $key = "ez-role-assignment-${groupId}-by-group-inherited"; } else { - $cacheItem = $this->cache->getItem("ez-role-assignment-${groupId}-by-group"); - } - - if ($cacheItem->isHit()) { - return $cacheItem->get(); - } - - $this->logger->logCall(__METHOD__, array('group' => $groupId, 'inherit' => $inherit)); - $roleAssignments = $this->persistenceHandler->userHandler()->loadRoleAssignmentsByGroupId($groupId, $inherit); - - $cacheItem->set($roleAssignments); - $cacheTags = [ - 'role-assignment-group-list-' . $groupId, /* empty results, non empty it might have duplicated tags but cache will reduce those. */ - 'role-' . $groupId, /* Role update (policies) changes role assignment id */ - ]; - foreach ($roleAssignments as $roleAssignment) { - $cacheTags = $this->getCacheTagsForRoleAssignment($roleAssignment, $cacheTags); - } - - // To make sure tree operations affecting this can clear the permission cache - $locations = $this->persistenceHandler->locationHandler()->loadLocationsByContent($groupId); - foreach ($locations as $location) { - foreach (explode('/', trim($location->pathString, '/')) as $pathId) { - $cacheTags[] = 'location-path-' . $pathId; - } + $key = "ez-role-assignment-${groupId}-by-group"; } - $cacheItem->tag($cacheTags); - $this->cache->save($cacheItem); - - return $roleAssignments; + return $this->getListCacheValue( + $key, + function () use ($groupId, $inherit) { + return $this->persistenceHandler->userHandler()->loadRoleAssignmentsByGroupId($groupId, $inherit); + }, + $this->getRoleAssignmentTags, + $this->getRoleAssignmentKeys, + static function () use ($groupId, $innerHandler) { + // To make sure tree operations affecting this can clear the permission cache + $cacheTags = []; + $locations = $innerHandler->locationHandler()->loadLocationsByContent($groupId); + foreach ($locations as $location) { + foreach (explode('/', trim($location->pathString, '/')) as $pathId) { + $cacheTags[] = 'location-path-' . $pathId; + } + } + + return $cacheTags; + }, + [$groupId, $inherit] + ); } /** @@ -366,7 +401,7 @@ public function updateRole(RoleUpdateStruct $struct) $this->logger->logCall(__METHOD__, array('struct' => $struct)); $this->persistenceHandler->userHandler()->updateRole($struct); - $this->cache->invalidateTags(['role-' . $struct->id]); + $this->invalidateCache(['role-' . $struct->id]); } /** @@ -378,7 +413,7 @@ public function deleteRole($roleId, $status = Role::STATUS_DEFINED) $return = $this->persistenceHandler->userHandler()->deleteRole($roleId, $status); if ($status === Role::STATUS_DEFINED) { - $this->cache->invalidateTags(['role-' . $roleId, 'role-assignment-role-list-' . $roleId]); + $this->invalidateCache(['role-' . $roleId, 'role-assignment-role-list-' . $roleId]); } return $return; @@ -396,7 +431,7 @@ public function publishRoleDraft($roleDraftId) // If there was a original role for the draft, then we clean cache for it if ($roleDraft->originalId > -1) { - $this->cache->invalidateTags(['role-' . $roleDraft->originalId]); + $this->invalidateCache(['role-' . $roleDraft->originalId]); } return $return; @@ -420,7 +455,7 @@ public function addPolicy($roleId, Policy $policy) $this->logger->logCall(__METHOD__, array('role' => $roleId, 'struct' => $policy)); $return = $this->persistenceHandler->userHandler()->addPolicy($roleId, $policy); - $this->cache->invalidateTags(['role-' . $roleId]); + $this->invalidateCache(['role-' . $roleId]); return $return; } @@ -433,7 +468,7 @@ public function updatePolicy(Policy $policy) $this->logger->logCall(__METHOD__, array('struct' => $policy)); $return = $this->persistenceHandler->userHandler()->updatePolicy($policy); - $this->cache->invalidateTags(['policy-' . $policy->id, 'role-' . $policy->roleId]); + $this->invalidateCache(['policy-' . $policy->id, 'role-' . $policy->roleId]); return $return; } @@ -446,7 +481,7 @@ public function deletePolicy($policyId, $roleId) $this->logger->logCall(__METHOD__, array('policy' => $policyId)); $this->persistenceHandler->userHandler()->deletePolicy($policyId, $roleId); - $this->cache->invalidateTags(['policy-' . $policyId, 'role-' . $roleId]); + $this->invalidateCache(['policy-' . $policyId, 'role-' . $roleId]); } /** @@ -473,7 +508,7 @@ public function assignRole($contentId, $roleId, array $limitation = null) $tags[] = 'location-path-' . $location->id; } - $this->cache->invalidateTags($tags); + $this->invalidateCache($tags); return $return; } @@ -486,7 +521,7 @@ public function unassignRole($contentId, $roleId) $this->logger->logCall(__METHOD__, array('group' => $contentId, 'role' => $roleId)); $return = $this->persistenceHandler->userHandler()->unassignRole($contentId, $roleId); - $this->cache->invalidateTags(['role-assignment-group-list-' . $contentId, 'role-assignment-role-list-' . $roleId]); + $this->invalidateCache(['role-assignment-group-list-' . $contentId, 'role-assignment-role-list-' . $roleId]); return $return; } @@ -499,25 +534,8 @@ public function removeRoleAssignment($roleAssignmentId) $this->logger->logCall(__METHOD__, array('assignment' => $roleAssignmentId)); $return = $this->persistenceHandler->userHandler()->removeRoleAssignment($roleAssignmentId); - $this->cache->invalidateTags(['role-assignment-' . $roleAssignmentId]); + $this->invalidateCache(['role-assignment-' . $roleAssignmentId]); return $return; } - - /** - * Reusable function to return relevant role assignment tags so cache can be purged reliably. - * - * @param \eZ\Publish\SPI\Persistence\User\RoleAssignment $roleAssignment - * @param array $tags Optional, can be used to specify other tags. - * - * @return array - */ - private function getCacheTagsForRoleAssignment(RoleAssignment $roleAssignment, array $tags = []) - { - $tags[] = 'role-assignment-' . $roleAssignment->id; - $tags[] = 'role-assignment-group-list-' . $roleAssignment->contentId; - $tags[] = 'role-assignment-role-list-' . $roleAssignment->roleId; - - return $tags; - } } diff --git a/eZ/Publish/Core/settings/storage_engines/cache.yml b/eZ/Publish/Core/settings/storage_engines/cache.yml index cdba0c07b9c..94a7fd79212 100644 --- a/eZ/Publish/Core/settings/storage_engines/cache.yml +++ b/eZ/Publish/Core/settings/storage_engines/cache.yml @@ -92,7 +92,7 @@ services: ezpublish.spi.persistence.cache.userHandler: class: "%ezpublish.spi.persistence.cache.userHandler.class%" - parent: ezpublish.spi.persistence.cache.abstractHandler + parent: ezpublish.spi.persistence.cache.abstractInMemoryCacheHandler ezpublish.spi.persistence.cache.transactionhandler: class: "%ezpublish.spi.persistence.cache.transactionhandler.class%" From ecda5536bb130fd6fabd94128a05051882777021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20R?= Date: Thu, 14 Mar 2019 06:54:16 +0100 Subject: [PATCH 11/17] Update panel.twig text on disabled logging --- .../Resources/views/Profiler/persistence/panel.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/persistence/panel.html.twig b/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/persistence/panel.html.twig index ed46225c9ce..9763a64f679 100644 --- a/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/persistence/panel.html.twig +++ b/eZ/Bundle/EzPublishDebugBundle/Resources/views/Profiler/persistence/panel.html.twig @@ -85,5 +85,5 @@ {% else %} -

TIP: Call logging is disabled by default as it consumes considerably memory, enable @TODO in order to see calls made and trace for where the calls comes from.

+

NOTE: Call logging is by default only enabled in debug mode as ezpublish.spi.persistence.cache.persistenceLogger.enableCallLogging: "%kernel.debug%", enable debug or change the setting in order to see calls made and trace for where the calls comes from.

{% endif %} From 8bb8c164e7f9c3aede57e236a2c452664f675004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20R?= Date: Thu, 14 Mar 2019 10:07:15 +0100 Subject: [PATCH 12/17] CS --- .../Core/Persistence/Cache/Tests/PersistenceLoggerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eZ/Publish/Core/Persistence/Cache/Tests/PersistenceLoggerTest.php b/eZ/Publish/Core/Persistence/Cache/Tests/PersistenceLoggerTest.php index 40c4c783f7f..9f9cde8f4ed 100644 --- a/eZ/Publish/Core/Persistence/Cache/Tests/PersistenceLoggerTest.php +++ b/eZ/Publish/Core/Persistence/Cache/Tests/PersistenceLoggerTest.php @@ -97,7 +97,7 @@ public function testGetCountValues($logger) */ public function testGetCallValues($logger) { - $calls = $logger->getCalls(); + $calls = $logger->getCalls(); // As we don't care about the hash index we get the array values instead $calls = array_values($calls); From 91293ef693091c83074fa0b5f60c3f84dddc46e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20R?= Date: Thu, 14 Mar 2019 10:43:05 +0100 Subject: [PATCH 13/17] [Test] Fix broken LanguageServiceTest casuing issues on Postgres --- eZ/Publish/API/Repository/Tests/LanguageServiceTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eZ/Publish/API/Repository/Tests/LanguageServiceTest.php b/eZ/Publish/API/Repository/Tests/LanguageServiceTest.php index e4570f3ba19..7cb4cb61233 100644 --- a/eZ/Publish/API/Repository/Tests/LanguageServiceTest.php +++ b/eZ/Publish/API/Repository/Tests/LanguageServiceTest.php @@ -393,7 +393,7 @@ public function testLoadLanguageThrowsNotFoundException() $languageService = $repository->getContentLanguageService(); - $languages = $languageService->loadLanguageListById(['fre-FR']); + $languages = $languageService->loadLanguageListByCode(['fre-FR']); $this->assertInternalType('iterable', $languages); $this->assertCount(0, $languages); From d4f29971f893359e4b135fc1ed43de63f035057a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20W=C3=B3js?= Date: Thu, 14 Mar 2019 17:54:58 +0100 Subject: [PATCH 14/17] Update eZ/Publish/Core/Persistence/Cache/InMemory/InMemoryCache.php Co-Authored-By: andrerom --- eZ/Publish/Core/Persistence/Cache/InMemory/InMemoryCache.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eZ/Publish/Core/Persistence/Cache/InMemory/InMemoryCache.php b/eZ/Publish/Core/Persistence/Cache/InMemory/InMemoryCache.php index e4b36257cf8..9b5310da99d 100644 --- a/eZ/Publish/Core/Persistence/Cache/InMemory/InMemoryCache.php +++ b/eZ/Publish/Core/Persistence/Cache/InMemory/InMemoryCache.php @@ -54,7 +54,7 @@ class InMemoryCache /** * In Memory Cache constructor. * - * @param float $ttl Seconds for the cache to live as a float, by default 0.3 (300 milliseconds) + * @param int $ttl Seconds for the cache to live, by default 300 milliseconds * @param int $limit Limit for values to keep in cache, by default 100 cache values (per pool instance). * @param bool $enabled For use by configuration to be able to disable or enable depending on needs. */ From c4a2ef8840afcd9bc86520b9d84d74b24203f1bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20R?= Date: Thu, 14 Mar 2019 21:46:45 +0100 Subject: [PATCH 15/17] Fix review comments --- .../Collector/PersistenceCacheCollector.php | 1 - .../Persistence/Cache/InMemory/InMemoryCache.php | 2 +- .../Legacy/Content/Language/CachingHandler.php | 12 +++++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/eZ/Bundle/EzPublishDebugBundle/Collector/PersistenceCacheCollector.php b/eZ/Bundle/EzPublishDebugBundle/Collector/PersistenceCacheCollector.php index a761c4a7483..f7ab0fab70d 100644 --- a/eZ/Bundle/EzPublishDebugBundle/Collector/PersistenceCacheCollector.php +++ b/eZ/Bundle/EzPublishDebugBundle/Collector/PersistenceCacheCollector.php @@ -106,7 +106,6 @@ public function getCalls() // Leave out in-memory lookups from sorting $count[$hash] = $call['stats']['uncached'] + $call['stats']['miss'] + $call['stats']['hit']; } - unset($data); array_multisort($count, SORT_DESC, $calls); diff --git a/eZ/Publish/Core/Persistence/Cache/InMemory/InMemoryCache.php b/eZ/Publish/Core/Persistence/Cache/InMemory/InMemoryCache.php index 9b5310da99d..63963309f91 100644 --- a/eZ/Publish/Core/Persistence/Cache/InMemory/InMemoryCache.php +++ b/eZ/Publish/Core/Persistence/Cache/InMemory/InMemoryCache.php @@ -133,7 +133,7 @@ public function setMulti(array $objects, callable $objectIndexes, string $listIn continue; } - $key = array_shift($indexes); + $key = \array_shift($indexes); $this->cache[$key] = $object; $this->cacheTime[$key] = $time; diff --git a/eZ/Publish/Core/Persistence/Legacy/Content/Language/CachingHandler.php b/eZ/Publish/Core/Persistence/Legacy/Content/Language/CachingHandler.php index 185b6b275d1..048df18919e 100644 --- a/eZ/Publish/Core/Persistence/Legacy/Content/Language/CachingHandler.php +++ b/eZ/Publish/Core/Persistence/Legacy/Content/Language/CachingHandler.php @@ -189,14 +189,20 @@ public function delete($id) } /** - * Clear internal cache. + * Clear internal in-memory cache. */ - public function clearCache() + public function clearCache(): void { $this->cache->clear(); } - protected function storeCache(array $languages, string $listIndex = null) + /** + * Helper to store languages in internal in-memory cache with all needed keys. + * + * @param array $languages + * @param string|null $listIndex + */ + protected function storeCache(array $languages, string $listIndex = null): void { $this->cache->setMulti( $languages, From 494c257cca693b6e69b5ca3a3c7f2d76f1b70eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20R?= Date: Fri, 15 Mar 2019 11:23:07 +0100 Subject: [PATCH 16/17] Change to callable for $listTags --- .../Core/Persistence/Cache/AbstractInMemoryHandler.php | 8 ++++---- eZ/Publish/Core/Persistence/Cache/ContentTypeHandler.php | 4 ++-- eZ/Publish/Core/Persistence/Cache/UserHandler.php | 5 ++--- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/eZ/Publish/Core/Persistence/Cache/AbstractInMemoryHandler.php b/eZ/Publish/Core/Persistence/Cache/AbstractInMemoryHandler.php index 2eb424d9fc7..c002a161480 100644 --- a/eZ/Publish/Core/Persistence/Cache/AbstractInMemoryHandler.php +++ b/eZ/Publish/Core/Persistence/Cache/AbstractInMemoryHandler.php @@ -125,7 +125,7 @@ final protected function getCacheValue( * @param callable $backendLoader Function for loading ALL objects, value is cached as-is. * @param callable $cacheTagger Gets cache object as argument, return array of cache tags. * @param callable $cacheIndexes Gets cache object as argument, return array of cache keys. - * @param array|callable $listTags Optional, global tags for the list cache, either as array or lazy callable. + * @param callable $listTags Optional, global tags for the list cache. * @param array $arguments Optional, arguments when parnt method takes arguments that key varies on. * * @return object @@ -135,7 +135,7 @@ final protected function getListCacheValue( callable $backendLoader, callable $cacheTagger, callable $cacheIndexes, - $listTags = [], + callable $listTags = null, array $arguments = [] ) { // In-memory @@ -159,10 +159,10 @@ final protected function getListCacheValue( $this->inMemory->setMulti($objects, $cacheIndexes, $key); $this->logger->logCacheMiss($arguments, 3); - if (is_callable($listTags)) { + if ($listTags !== null) { $tagSet = [$listTags()]; } else { - $tagSet = [$listTags]; + $tagSet = [[]]; } foreach ($objects as $object) { diff --git a/eZ/Publish/Core/Persistence/Cache/ContentTypeHandler.php b/eZ/Publish/Core/Persistence/Cache/ContentTypeHandler.php index 350247aa694..8428c81ff63 100644 --- a/eZ/Publish/Core/Persistence/Cache/ContentTypeHandler.php +++ b/eZ/Publish/Core/Persistence/Cache/ContentTypeHandler.php @@ -189,7 +189,7 @@ function () use ($groupId, $status) { }, $this->getTypeTags, $this->getTypeKeys, - ['type-group-' . $groupId], + static function () use ($groupId) { return ['type-group-' . $groupId]; }, [$groupId] ); } @@ -490,7 +490,7 @@ function () { }, static function () {return [];}, static function () {return [];}, - ['type-map'] + static function () { return ['type-map']; } ); } diff --git a/eZ/Publish/Core/Persistence/Cache/UserHandler.php b/eZ/Publish/Core/Persistence/Cache/UserHandler.php index 662aa5e7012..dffdaf5c7dc 100644 --- a/eZ/Publish/Core/Persistence/Cache/UserHandler.php +++ b/eZ/Publish/Core/Persistence/Cache/UserHandler.php @@ -351,9 +351,8 @@ function () use ($roleId) { }, $this->getRoleAssignmentTags, $this->getRoleAssignmentKeys, - [ - 'role-' . $roleId, /* Role update (policies) changes role assignment id */ - ], + /* Role update (policies) changes role assignment id */ + static function () use ($roleId) { return ['role-' . $roleId]; }, [$roleId] ); } From f40521b6d4e64d6f0a471c1d353afa9b2a05cdd7 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Mon, 18 Mar 2019 16:51:07 +0100 Subject: [PATCH 17/17] Update eZ/Publish/Core/settings/storage_engines/cache.yml Co-Authored-By: andrerom --- eZ/Publish/Core/settings/storage_engines/cache.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eZ/Publish/Core/settings/storage_engines/cache.yml b/eZ/Publish/Core/settings/storage_engines/cache.yml index 94a7fd79212..f013f76830b 100644 --- a/eZ/Publish/Core/settings/storage_engines/cache.yml +++ b/eZ/Publish/Core/settings/storage_engines/cache.yml @@ -37,7 +37,7 @@ services: class: "%ezpublish.cache_pool.driver.class%" arguments: ["", 120] - # Logger which logs info when in dev for synfony web toolbar + # Logger which logs info when in dev for Symfony web toolbar ezpublish.spi.persistence.cache.persistenceLogger: class: "%ezpublish.spi.persistence.cache.persistenceLogger.class%" arguments: