Skip to content

Commit

Permalink
Merge pull request #3631 from Sebobo/feature/flush-tags
Browse files Browse the repository at this point in the history
FEATURE: Pass tags to be flushed to content cache backend
  • Loading branch information
kdambekalns committed Mar 23, 2022
2 parents 18e16b8 + d5f8563 commit 52cdc91
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 63 deletions.
11 changes: 11 additions & 0 deletions Neos.Fusion/Classes/Core/Cache/ContentCache.php
Expand Up @@ -382,6 +382,17 @@ public function flushByTag($tag)
return $this->cache->flushByTag($this->sanitizeTag($tag));
}

/**
* Flush content cache entries by tags
*
* @param array<string> $tags values that were assigned to a cache entry in Fusion, for example "Everything", "Node_[…]", "NodeType_[…]", "DescendantOf_[…]" whereas "…" is the node identifier or node type respectively
* @return integer The number of cache entries which actually have been flushed
*/
public function flushByTags(array $tags): int
{
return $this->cache->flushByTags($this->sanitizeTags($tags));
}

/**
* Flush all content cache entries
*
Expand Down
107 changes: 48 additions & 59 deletions Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php
Expand Up @@ -13,6 +13,7 @@

use Neos\ContentRepository\Domain\Model\Workspace;
use Neos\ContentRepository\Domain\Repository\WorkspaceRepository;
use Neos\ContentRepository\Exception\NodeTypeNotFoundException;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Log\Utility\LogEnvironment;
use Neos\Flow\Persistence\PersistenceManagerInterface;
Expand Down Expand Up @@ -54,7 +55,7 @@ class ContentCacheFlusher
protected $systemLogger;

/**
* @var array
* @var array<string, string>
*/
protected $tagsToFlush = [];

Expand Down Expand Up @@ -109,18 +110,22 @@ class ContentCacheFlusher
*/
protected $securityContext;

/**
* @Flow\InjectConfiguration(path="fusion.contentCacheDebugMode")
* @var bool
*/
protected $debugMode;

/**
* Register a node change for a later cache flush. This method is triggered by a signal sent via ContentRepository's Node
* model or the Neos Publishing Service.
*
* @param NodeInterface $node The node which has changed in some way
* @param Workspace $targetWorkspace An optional workspace to flush
* @return void
* @throws \Neos\ContentRepository\Exception\NodeTypeNotFoundException
* @param Workspace|null $targetWorkspace An optional workspace to flush
*/
public function registerNodeChange(NodeInterface $node, Workspace $targetWorkspace = null): void
{
$this->tagsToFlush[ContentCache::TAG_EVERYTHING] = 'which were tagged with "Everything".';
$this->addTagToFlush(ContentCache::TAG_EVERYTHING, 'which were tagged with "Everything".');

if (empty($this->workspacesToFlush[$node->getWorkspace()->getName()])) {
$this->resolveWorkspaceChain($node->getWorkspace());
Expand All @@ -140,10 +145,11 @@ public function registerNodeChange(NodeInterface $node, Workspace $targetWorkspa
}
}

/**
* @param NodeInterface $node
* @param Workspace $workspace
*/
protected function addTagToFlush(string $tag, string $message = ''): void
{
$this->tagsToFlush[$tag] = $this->debugMode ? $message : '';
}

protected function registerAllTagsToFlushForNodeInWorkspace(NodeInterface $node, Workspace $workspace): void
{
$nodeIdentifier = $node->getIdentifier();
Expand All @@ -168,32 +174,23 @@ protected function registerAllTagsToFlushForNodeInWorkspace(NodeInterface $node,
break;
}
$tagName = 'DescendantOf_' . $workspaceHash . '_' . $nodeInWorkspace->getIdentifier();
$this->tagsToFlush[$tagName] = sprintf('which were tagged with "%s" because node "%s" has changed.', $tagName, $node->getPath());
$this->addTagToFlush($tagName, sprintf('which were tagged with "%s" because node "%s" has changed.', $tagName, $node->getPath()));

$legacyTagName = 'DescendantOf_' . $nodeInWorkspace->getIdentifier();
$this->tagsToFlush[$legacyTagName] = sprintf('which were tagged with legacy "%s" because node "%s" has changed.', $legacyTagName, $node->getPath());
$this->addTagToFlush($legacyTagName, sprintf('which were tagged with legacy "%s" because node "%s" has changed.', $legacyTagName, $node->getPath()));
}
}
}

/**
* @param Workspace $workspace
* @return void
*/
protected function resolveWorkspaceChain(Workspace $workspace)
protected function resolveWorkspaceChain(Workspace $workspace): void
{
$cachingHelper = $this->getCachingHelper();

$this->workspacesToFlush[$workspace->getName()][$workspace->getName()] = $cachingHelper->renderWorkspaceTagForContextNode($workspace->getName());
$this->resolveTagsForChildWorkspaces($workspace, $workspace->getName());
}

/**
* @param Workspace $workspace
* @param string $startingPoint
* @return void
*/
protected function resolveTagsForChildWorkspaces(Workspace $workspace, string $startingPoint)
protected function resolveTagsForChildWorkspaces(Workspace $workspace, string $startingPoint): void
{
$cachingHelper = $this->getCachingHelper();
$this->workspacesToFlush[$startingPoint][$workspace->getName()] = $cachingHelper->renderWorkspaceTagForContextNode($workspace->getName());
Expand All @@ -210,54 +207,46 @@ protected function resolveTagsForChildWorkspaces(Workspace $workspace, string $s
* Pleas use registerNodeChange() if possible. This method is a low-level api. If you do use this method make sure
* that $cacheIdentifier contains the workspacehash as well as the node identifier: $workspaceHash .'_'. $nodeIdentifier
* The workspacehash can be received via $this->getCachingHelper()->renderWorkspaceTagForContextNode($workpsacename)
*
* @param string $cacheIdentifier
*/
public function registerChangeOnNodeIdentifier($cacheIdentifier)
public function registerChangeOnNodeIdentifier(string $cacheIdentifier): void
{
$this->tagsToFlush[ContentCache::TAG_EVERYTHING] = 'which were tagged with "Everything".';
$this->tagsToFlush['Node_' . $cacheIdentifier] = sprintf('which were tagged with "Node_%s" because that identifier has changed.', $cacheIdentifier);
$this->tagsToFlush['NodeDynamicTag_' . $cacheIdentifier] = sprintf('which were tagged with "NodeDynamicTag_%s" because that identifier has changed.', $cacheIdentifier);
$this->addTagToFlush(ContentCache::TAG_EVERYTHING, 'which were tagged with "Everything".');
$this->addTagToFlush('Node_' . $cacheIdentifier, sprintf('which were tagged with "Node_%s" because that identifier has changed.', $cacheIdentifier));
$this->addTagToFlush('NodeDynamicTag_' . $cacheIdentifier, sprintf('which were tagged with "NodeDynamicTag_%s" because that identifier has changed.', $cacheIdentifier));

// Note, as we don't have a node here we cannot go up the structure.
$tagName = 'DescendantOf_' . $cacheIdentifier;
$this->tagsToFlush[$tagName] = sprintf('which were tagged with "%s" because node "%s" has changed.', $tagName, $cacheIdentifier);
$this->addTagToFlush($tagName, sprintf('which were tagged with "%s" because node "%s" has changed.', $tagName, $cacheIdentifier));
}

/**
* This is a low-level api. Please use registerNodeChange() if possible. Otherwise make sure that $nodeTypePrefix
* is set up correctly and contains the workspacehash wich can be received via
* $this->getCachingHelper()->renderWorkspaceTagForContextNode($workpsacename)
*
* @param string $nodeTypeName
* @param string $referenceNodeIdentifier
* @param string $nodeTypePrefix
*
* @throws \Neos\ContentRepository\Exception\NodeTypeNotFoundException
* @throws NodeTypeNotFoundException
*/
public function registerChangeOnNodeType($nodeTypeName, $referenceNodeIdentifier = null, $nodeTypePrefix = '')
public function registerChangeOnNodeType(string $nodeTypeName, string $referenceNodeIdentifier = null, string $nodeTypePrefix = ''): void
{
$this->tagsToFlush[ContentCache::TAG_EVERYTHING] = 'which were tagged with "Everything".';
$this->addTagToFlush(ContentCache::TAG_EVERYTHING, 'which were tagged with "Everything".');

$nodeTypesToFlush = $this->getAllImplementedNodeTypeNames($this->nodeTypeManager->getNodeType($nodeTypeName));

if (strlen($nodeTypePrefix) > 0) {
if ($nodeTypePrefix !== '') {
$nodeTypePrefix = rtrim($nodeTypePrefix, '_') . '_';
}

foreach ($nodeTypesToFlush as $nodeTypeNameToFlush) {
$this->tagsToFlush['NodeType_' . $nodeTypePrefix . $nodeTypeNameToFlush] = sprintf('which were tagged with "NodeType_%s" because node "%s" has changed and was of type "%s".', $nodeTypeNameToFlush, ($referenceNodeIdentifier ? $referenceNodeIdentifier : ''), $nodeTypeName);
$this->addTagToFlush('NodeType_' . $nodeTypePrefix . $nodeTypeNameToFlush, sprintf('which were tagged with "NodeType_%s" because node "%s" has changed and was of type "%s".', $nodeTypeNameToFlush, ($referenceNodeIdentifier ? $referenceNodeIdentifier : ''), $nodeTypeName));
}
}

/**
* Fetches possible usages of the asset and registers nodes that use the asset as changed.
*
* @param AssetInterface $asset
* @return void
* @throws \Neos\ContentRepository\Exception\NodeTypeNotFoundException
* @throws NodeTypeNotFoundException
*/
public function registerAssetChange(AssetInterface $asset)
public function registerAssetChange(AssetInterface $asset): void
{
// In Nodes only assets are referenced, never asset variants directly. When an asset
// variant is updated, it is passed as $asset, but since it is never "used" by any node
Expand Down Expand Up @@ -293,31 +282,35 @@ public function registerAssetChange(AssetInterface $asset)
$assetIdentifier = $this->persistenceManager->getIdentifierByObject($asset);
// @see RuntimeContentCache.addTag
$tagName = 'AssetDynamicTag_' . $workspaceHash . '_' . $assetIdentifier;
$this->tagsToFlush[$tagName] = sprintf('which were tagged with "%s" because asset "%s" has changed.', $tagName, $assetIdentifier);
$this->addTagToFlush($tagName, sprintf('which were tagged with "%s" because asset "%s" has changed.', $tagName, $assetIdentifier));
}
}

public function shutdownObject(): void
{
$this->commit();
}

/**
* Flush caches according to the previously registered node changes.
*
* @return void
*/
public function shutdownObject()
protected function commit(): void
{
if ($this->tagsToFlush !== []) {
foreach ($this->tagsToFlush as $tag => $logMessage) {
$affectedEntries = $this->contentCache->flushByTag($tag);
if ($affectedEntries > 0) {
$this->systemLogger->debug(sprintf('Content cache: Removed %s entries %s', $affectedEntries, $logMessage));
if ($this->debugMode) {
foreach ($this->tagsToFlush as $tag => $logMessage) {
$affectedEntries = $this->contentCache->flushByTag($tag);
if ($affectedEntries > 0) {
$this->systemLogger->debug(sprintf('Content cache: Removed %s entries %s', $affectedEntries, $logMessage));
}
}
} else {
$affectedEntries = $this->contentCache->flushByTags(array_keys($this->tagsToFlush));
$this->systemLogger->debug(sprintf('Content cache: Removed %s entries', $affectedEntries));
}
}
}

/**
* @param AssetUsageInNodeProperties $assetUsage
* @return ContentContext
*/
protected function getContextForReference(AssetUsageInNodeProperties $assetUsage): ContentContext
{
$hash = md5(sprintf('%s-%s', $assetUsage->getWorkspaceName(), json_encode($assetUsage->getDimensionValues())));
Expand All @@ -334,10 +327,9 @@ protected function getContextForReference(AssetUsageInNodeProperties $assetUsage
}

/**
* @param NodeType $nodeType
* @return array<string>
*/
protected function getAllImplementedNodeTypeNames(NodeType $nodeType)
protected function getAllImplementedNodeTypeNames(NodeType $nodeType): array
{
$self = $this;
$types = array_reduce($nodeType->getDeclaredSuperTypes(), function (array $types, NodeType $superType) use ($self) {
Expand All @@ -348,9 +340,6 @@ protected function getAllImplementedNodeTypeNames(NodeType $nodeType)
return $types;
}

/**
* @return CachingHelper
*/
protected function getCachingHelper(): CachingHelper
{
if (!$this->cachingHelper instanceof CachingHelper) {
Expand Down
4 changes: 1 addition & 3 deletions Neos.Neos/Classes/Routing/Cache/RouteCacheFlusher.php
Expand Up @@ -77,9 +77,7 @@ public function registerBaseWorkspaceChange(Workspace $workspace, Workspace $old
*/
public function commit()
{
foreach ($this->tagsToFlush as $tag) {
$this->routeCachingService->flushCachesByTag($tag);
}
$this->routeCachingService->flushCachesByTags($this->tagsToFlush);
$this->tagsToFlush = [];
}

Expand Down
5 changes: 5 additions & 0 deletions Neos.Neos/Configuration/Settings.yaml
Expand Up @@ -21,6 +21,11 @@ Neos:
# - in Production context
enableObjectTreeCache: false

# If set to true, content cache flushes will be done on a per-tag basis and generate additional log output
# which allows understanding why flushed entries were flushed. This is useful for debugging but will
# hurt performance and should not be used in production.
contentCacheDebugMode: false

# Packages can now register with this setting to get their Fusion in the path:
# resources://MyVendor.MyPackageKey/Private/Fusion/Root.fusion
# included automatically.
Expand Down
Expand Up @@ -16,7 +16,6 @@
use Neos\ContentRepository\Domain\Model\Workspace;
use Neos\Flow\Tests\UnitTestCase;
use Neos\Neos\Fusion\Cache\ContentCacheFlusher;
use Neos\Neos\Fusion\Helper\CachingHelper;

/**
* Tests the CachingHelper
Expand Down Expand Up @@ -44,6 +43,7 @@ public function theWorkspaceChainWillOnlyEvaluatedIfNeeded()
$nodeMock = $this->getMockBuilder(NodeInterface::class)->disableOriginalConstructor()->getMock();
$nodeMock->expects(self::any())->method('getWorkspace')->willReturn($workspace);
$nodeMock->expects(self::any())->method('getNodeType')->willReturn($nodeType);
$nodeMock->expects(self::any())->method('getIdentifier')->willReturn('some-node-identifier');

$contentCacheFlusher->registerNodeChange($nodeMock);
}
Expand Down

0 comments on commit 52cdc91

Please sign in to comment.