Skip to content

Commit

Permalink
FEATURE: CacheEntires should respect the workspace they rely on
Browse files Browse the repository at this point in the history
Before this change there are no cache entries based on a workspace. So editing a node in the neos backend would always flush the live cache even if you do not publish the change to live. This happens as the we do not respect the current workspace in our cache tags.

This change will adjust all generated content cache tags and respect the related workspace. The `ContentCacheFlusher` will also only flush cache entries for the current workspace. As a fallback we still write and flush the old tags.
To make use of the new cache tags inside fusion use the CachingHelper like this `${Neos.Caching.nodeTag(node)}`. This will create all correct cache tags wich are needed.

Resolves neos#2096
  • Loading branch information
johannessteu committed Jun 27, 2018
1 parent 60050c9 commit e5e6ef2
Show file tree
Hide file tree
Showing 6 changed files with 427 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@
* source code.
*/

use Neos\ContentRepository\Domain\Model\Workspace;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Persistence\QueryInterface;
use Neos\Flow\Persistence\QueryResultInterface;
use Neos\Flow\Persistence\Repository;

/**
* The repository for workspaces
*
* @Flow\Scope("singleton")
* @method QueryResultInterface findByBaseWorkspace($baseWorkspace)
* @method Workspace findByIdentifier($baseWorkspace)
*/
class WorkspaceRepository extends Repository
{
Expand Down
141 changes: 117 additions & 24 deletions Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
* source code.
*/

use Neos\ContentRepository\Domain\Model\Workspace;
use Neos\ContentRepository\Domain\Repository\WorkspaceRepository;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Log\SystemLoggerInterface;
use Neos\Flow\Log\PsrSystemLoggerInterface;
use Neos\Flow\Persistence\PersistenceManagerInterface;
use Neos\Media\Domain\Model\AssetInterface;
use Neos\Media\Domain\Service\AssetService;
Expand All @@ -21,6 +23,7 @@
use Neos\ContentRepository\Domain\Model\NodeType;
use Neos\ContentRepository\Domain\Service\NodeTypeManager;
use Neos\Fusion\Core\Cache\ContentCache;
use Neos\Neos\Fusion\Helper\CachingHelper;

/**
* This service flushes Fusion content caches triggered by node changes.
Expand All @@ -41,14 +44,30 @@ class ContentCacheFlusher

/**
* @Flow\Inject
* @var SystemLoggerInterface
* @var PsrSystemLoggerInterface
*/
protected $systemLogger;

/**
* @var array
*/
protected $tagsToFlush = array();
protected $tagsToFlush = [];

/**
* @var CachingHelper
*/
protected $cachingHelper;

/**
* @Flow\Inject
* @var WorkspaceRepository
*/
protected $workspaceRepository;

/**
* @var array
*/
protected $workspacesToFlush = [];

/**
* @Flow\Inject
Expand All @@ -74,50 +93,106 @@ class ContentCacheFlusher
*
* @param NodeInterface $node The node which has changed in some way
* @return void
* @throws \Neos\ContentRepository\Exception\NodeTypeNotFoundException
*/
public function registerNodeChange(NodeInterface $node)
{
$this->tagsToFlush[ContentCache::TAG_EVERYTHING] = 'which were tagged with "Everything".';
$this->resolveWorkspaceChain($node->getWorkspace());

if (!array_key_exists($node->getWorkspace()->getName(), $this->workspacesToFlush)) {
return;
}

foreach ($this->workspacesToFlush[$node->getWorkspace()->getName()] as $workspaceName => $workspaceHash) {
$nodeIdentifier = $node->getIdentifier();

$this->registerChangeOnNodeIdentifier($workspaceHash .'_'. $nodeIdentifier);
$this->registerChangeOnNodeType($node->getNodeType()->getName(), $nodeIdentifier, $workspaceHash);

$nodeInWorkspace = $node;
while ($nodeInWorkspace->getDepth() > 1) {
$nodeInWorkspace = $nodeInWorkspace->getParent();
// Workaround for issue #56566 in Neos.ContentRepository
if ($nodeInWorkspace === null) {
break;
}
$tagName = 'DescendantOf_' . $workspaceHash . '_' . $nodeInWorkspace->getIdentifier();
$this->tagsToFlush[$tagName] = sprintf('which were tagged with "%s" because node "%s" has changed.', $tagName, $node->getPath());
}
}
}

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

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

$this->registerChangeOnNodeType($node->getNodeType()->getName(), $node->getIdentifier());
$this->registerChangeOnNodeIdentifier($node->getIdentifier());
/**
* @param Workspace $workspace
* @param string $startingPoint
* @return void
*/
protected function resolveTagsForChildWorkspaces(Workspace $workspace, string $startingPoint)
{
$cachingHelper = $this->getCachingHelper();
$this->workspacesToFlush[$startingPoint][$workspace->getName()] = $cachingHelper->renderWorkspaceTagForContextNode($workspace->getName());

$originalNode = $node;
while ($node->getDepth() > 1) {
$node = $node->getParent();
// Workaround for issue #56566 in Neos.ContentRepository
if ($node === null) {
break;
$childWorkspaces = $this->workspaceRepository->findByBaseWorkspace($workspace->getName());
if ($childWorkspaces->valid()) {
foreach ($childWorkspaces as $childWorkspace) {
$this->resolveTagsForChildWorkspaces($childWorkspace, $startingPoint);
}
$tagName = 'DescendantOf_' . $node->getIdentifier();
$this->tagsToFlush[$tagName] = sprintf('which were tagged with "%s" because node "%s" has changed.', $tagName, $originalNode->getPath());
}
}

/**
* @param string $nodeIdentifier
* 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($nodeIdentifier)
public function registerChangeOnNodeIdentifier($cacheIdentifier)
{
$this->tagsToFlush[ContentCache::TAG_EVERYTHING] = 'which were tagged with "Everything".';
$this->tagsToFlush['Node_' . $nodeIdentifier] = sprintf('which were tagged with "Node_%s" because that identifier has changed.', $nodeIdentifier);
$this->tagsToFlush['Node_' . $cacheIdentifier] = sprintf('which were tagged with "Node_%s" because that identifier has changed.', $cacheIdentifier);

// Note, as we don't have a node here we cannot go up the structure.
$tagName = 'DescendantOf_' . $nodeIdentifier;
$this->tagsToFlush[$tagName] = sprintf('which were tagged with "%s" because node "%s" has changed.', $tagName, $nodeIdentifier);
$tagName = 'DescendantOf_' . $cacheIdentifier;
$this->tagsToFlush[$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
*/
public function registerChangeOnNodeType($nodeTypeName, $referenceNodeIdentifier = null)
public function registerChangeOnNodeType($nodeTypeName, $referenceNodeIdentifier = null, $nodeTypePrefix = '')
{
$this->tagsToFlush[ContentCache::TAG_EVERYTHING] = 'which were tagged with "Everything".';

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

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

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

Expand Down Expand Up @@ -145,16 +220,21 @@ public function registerAssetChange(AssetInterface $asset)
return;
}

$cachingHelper = $this->getCachingHelper();

foreach ($this->assetService->getUsageReferences($asset) as $reference) {
if (!$reference instanceof AssetUsageInNodeProperties) {
continue;
}

$this->registerChangeOnNodeIdentifier($reference->getNodeIdentifier());
$this->registerChangeOnNodeType($reference->getNodeTypeName(), $reference->getNodeIdentifier());
$workspaceHash = $cachingHelper->renderWorkspaceTagForContextNode($reference->getWorkspaceName());

$this->registerChangeOnNodeIdentifier($workspaceHash . '_' . $reference->getNodeIdentifier());
$this->registerChangeOnNodeType($reference->getNodeTypeName(), $reference->getNodeIdentifier(), $workspaceHash);

$assetIdentifier = $this->persistenceManager->getIdentifierByObject($asset);
$tagName = 'AssetDynamicTag_' . $assetIdentifier;

$tagName = 'AssetDynamicTag_' . $workspaceHash . '_' . $assetIdentifier;
$this->tagsToFlush[$tagName] = sprintf('which were tagged with "%s" because asset "%s" has changed.', $tagName, $assetIdentifier);
}
}
Expand All @@ -166,8 +246,9 @@ public function registerAssetChange(AssetInterface $asset)
*/
public function shutdownObject()
{
if ($this->tagsToFlush !== array()) {
if ($this->tagsToFlush !== []) {
foreach ($this->tagsToFlush as $tag => $logMessage) {
$this->systemLogger->log('flushing tag ' . $tag);
$affectedEntries = $this->contentCache->flushByTag($tag);
if ($affectedEntries > 0) {
$this->systemLogger->log(sprintf('Content cache: Removed %s entries %s', $affectedEntries, $logMessage), LOG_DEBUG);
Expand All @@ -190,4 +271,16 @@ protected function getAllImplementedNodeTypeNames(NodeType $nodeType)
$types = array_unique($types);
return $types;
}

/**
* @return CachingHelper
*/
protected function getCachingHelper(): CachingHelper
{
if (!$this->cachingHelper instanceof CachingHelper) {
$this->cachingHelper = new CachingHelper();
}

return $this->cachingHelper;
}
}
13 changes: 8 additions & 5 deletions Neos.Neos/Classes/Fusion/Helper/CachingHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* source code.
*/

use Neos\ContentRepository\Domain\Model\Workspace;
use Neos\Flow\Annotations as Flow;
use Neos\Eel\ProtectedContextAwareInterface;
use Neos\Neos\Exception;
Expand Down Expand Up @@ -49,7 +50,7 @@ protected function convertArrayOfNodesToArrayOfNodeIdentifiersWithPrefix($nodes,
if (!$node instanceof NodeInterface) {
throw new Exception(sprintf('One of the elements in array passed to this helper was not a Node, but of type: "%s".', gettype($node)), 1437169991);
}
$prefixedNodeIdentifiers[] = $prefix . '_' . $this->renderWorkspaceTagForContextNode($node) . '_' . $node->getIdentifier();
$prefixedNodeIdentifiers[] = $prefix . '_' . $this->renderWorkspaceTagForContextNode($node->getContext()->getWorkspace()->getName()) . '_' . $node->getIdentifier();
}
return $prefixedNodeIdentifiers;
}
Expand All @@ -61,6 +62,7 @@ protected function convertArrayOfNodesToArrayOfNodeIdentifiersWithPrefix($nodes,
*
* @param mixed $nodes (A single Node or array or \Traversable of Nodes)
* @return array
* @throws Exception
*/
public function nodeTag($nodes)
{
Expand Down Expand Up @@ -102,7 +104,7 @@ protected function getNodeTypeTagFor($nodeType, $contextNode = null)
$workspaceTag = '';

if ($contextNode instanceof NodeInterface) {
$workspaceTag = $this->renderWorkspaceTagForContextNode($contextNode) .'_';
$workspaceTag = $this->renderWorkspaceTagForContextNode($contextNode->getContext()->getWorkspace()->getName()) .'_';
}

if (is_string($nodeType)) {
Expand All @@ -127,19 +129,20 @@ protected function getNodeTypeTagFor($nodeType, $contextNode = null)
*
* @param mixed $nodes (A single Node or array or \Traversable of Nodes)
* @return array
* @throws Exception
*/
public function descendantOfTag($nodes)
{
return $this->convertArrayOfNodesToArrayOfNodeIdentifiersWithPrefix($nodes, 'DescendantOf');
}

/**
* @param NodeInterface $contextNode
* @param string $contextNode
* @return string
*/
public function renderWorkspaceTagForContextNode(NodeInterface $contextNode)
public function renderWorkspaceTagForContextNode(string $workspaceName)
{
return '%' . md5($contextNode->getWorkspace()->getName()) . '%';
return '%' . md5($workspaceName) . '%';
}

/**
Expand Down
Loading

0 comments on commit e5e6ef2

Please sign in to comment.