Skip to content

Commit

Permalink
FEATURE: Overhaul ContentCacheFlusher
Browse files Browse the repository at this point in the history
  • Loading branch information
dlubitz committed Aug 27, 2024
1 parent 251fc1b commit e2bc811
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,15 @@ public function registerAssetChange(AssetInterface $asset): void
$workspaceName,
$nodeAggregate->nodeAggregateId,
$nodeAggregate->nodeTypeName,
$this->determineParentNodeAggregateIds($contentRepository, $workspaceName, $nodeAggregate->nodeAggregateId),
$this->determineParentNodeAggregateIds($contentRepository, $workspaceName, $nodeAggregate->nodeAggregateId, NodeAggregateIds::createEmpty()),
);

$this->contentCacheFlusher->flushNodeAggregate($flushNodeAggregateRequest, CacheFlushingStrategy::ON_SHUTDOWN);
}
}
}

private function determineParentNodeAggregateIds(ContentRepository $contentRepository, WorkspaceName $workspaceName, NodeAggregateId $childNodeAggregateId): NodeAggregateIds
private function determineParentNodeAggregateIds(ContentRepository $contentRepository, WorkspaceName $workspaceName, NodeAggregateId $childNodeAggregateId, NodeAggregateIds $collectedParentNodeAggregateIds): NodeAggregateIds
{
$parentNodeAggregates = $contentRepository->getContentGraph($workspaceName)->findParentNodeAggregates($childNodeAggregateId);
$parentNodeAggregateIds = NodeAggregateIds::fromArray(
Expand All @@ -85,11 +86,13 @@ private function determineParentNodeAggregateIds(ContentRepository $contentRepos

foreach ($parentNodeAggregateIds as $parentNodeAggregateId) {
// Prevent infinite loops
if (!$parentNodeAggregateIds->contain($parentNodeAggregateId)) {
$parentNodeAggregateIds->merge($this->determineParentNodeAggregateIds($contentRepository, $workspaceName, $parentNodeAggregateId));
if (!$collectedParentNodeAggregateIds->contain($parentNodeAggregateId)) {
$collectedParentNodeAggregateIds = $collectedParentNodeAggregateIds->merge(NodeAggregateIds::create($parentNodeAggregateId));
$collectedParentNodeAggregateIds = $this->determineParentNodeAggregateIds($contentRepository, $workspaceName, $parentNodeAggregateId, $collectedParentNodeAggregateIds);
}
}

return $parentNodeAggregateIds;

return $collectedParentNodeAggregateIds;
}
}
5 changes: 3 additions & 2 deletions Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ public function flushWorkspace(

$nodeCacheIdentifier = CacheTag::forWorkspaceName($flushWorkspaceRequest->contentRepositoryId, $flushWorkspaceRequest->workspaceName);
$tagsToFlush[$nodeCacheIdentifier->value] = sprintf(
'which were tagged with "%s" because that identifier has changed.',
$nodeCacheIdentifier->value
'which were tagged with "%s" because something on the workspace "%s" has changed.',
$nodeCacheIdentifier->value,
$flushWorkspaceRequest->workspaceName->value
);

$this->flushTags($tagsToFlush, $cacheFlushingStrategy);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,20 @@
use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\EventStore\EventInterface;
use Neos\ContentRepository\Core\Feature\Common\EmbedsContentStreamAndNodeAggregateId;
use Neos\ContentRepository\Core\Feature\NodeCreation\Event\NodeAggregateWithNodeWasCreated;
use Neos\ContentRepository\Core\Feature\NodeModification\Event\NodePropertiesWereSet;
use Neos\ContentRepository\Core\Feature\NodeMove\Event\NodeAggregateWasMoved;
use Neos\ContentRepository\Core\Feature\NodeReferencing\Event\NodeReferencesWereSet;
use Neos\ContentRepository\Core\Feature\NodeRemoval\Event\NodeAggregateWasRemoved;
use Neos\ContentRepository\Core\Feature\NodeRenaming\Event\NodeAggregateNameWasChanged;
use Neos\ContentRepository\Core\Feature\NodeTypeChange\Event\NodeAggregateTypeWasChanged;
use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeGeneralizationVariantWasCreated;
use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodePeerVariantWasCreated;
use Neos\ContentRepository\Core\Feature\NodeVariation\Event\NodeSpecializationVariantWasCreated;
use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateDimensionsWereUpdated;
use Neos\ContentRepository\Core\Feature\RootNodeCreation\Event\RootNodeAggregateWithNodeWasCreated;
use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasTagged;
use Neos\ContentRepository\Core\Feature\SubtreeTagging\Event\SubtreeWasUntagged;
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Event\WorkspaceWasDiscarded;
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Event\WorkspaceWasPartiallyDiscarded;
use Neos\ContentRepository\Core\Projection\CatchUpHookInterface;
Expand Down Expand Up @@ -143,6 +155,27 @@ public function __construct(
) {
}

public function canHandle(EventInterface $event): bool
{
return in_array($event::class, [
NodeAggregateNameWasChanged::class,
NodeAggregateTypeWasChanged::class,
NodeAggregateWasMoved::class,
NodeAggregateWasRemoved::class,
NodeAggregateWithNodeWasCreated::class,
NodeGeneralizationVariantWasCreated::class,
NodePeerVariantWasCreated::class,
NodePropertiesWereSet::class,
NodeReferencesWereSet::class,
NodeSpecializationVariantWasCreated::class,
RootNodeAggregateDimensionsWereUpdated::class,
RootNodeAggregateWithNodeWasCreated::class,
SubtreeWasTagged::class,
SubtreeWasUntagged::class,
WorkspaceWasDiscarded::class,
WorkspaceWasPartiallyDiscarded::class
]);
}

public function onBeforeCatchUp(): void
{
Expand All @@ -156,26 +189,25 @@ public function onBeforeEvent(EventInterface $eventInstance, EventEnvelope $even
return;
}

if (!$this->canHandle($eventInstance)) {
return;
}

if (
$eventInstance instanceof NodeAggregateWasRemoved
// NOTE: when moving a node, we need to clear the cache not just after the move was completed,
// but also on the original location. Otherwise, we have the problem that the cache is not
// cleared, leading to presumably duplicate nodes in the UI.
|| $eventInstance instanceof NodeAggregateWasMoved
) {
$workspace = $this->contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId($eventInstance->getContentStreamId());
if ($workspace === null) {
return;
}
// FIXME: EventInterface->workspaceName
$contentGraph = $this->contentRepository->getContentGraph($workspace->workspaceName);
$contentGraph = $this->contentRepository->getContentGraph($eventInstance->workspaceName);
$nodeAggregate = $contentGraph->findNodeAggregateById(
$eventInstance->getNodeAggregateId()
);
if ($nodeAggregate) {
$this->scheduleCacheFlushJobForNodeAggregate(
$this->contentRepository,
$workspace->workspaceName,
$eventInstance->workspaceName,
$nodeAggregate
);
}
Expand All @@ -190,6 +222,10 @@ public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $event
return;
}

if (!$this->canHandle($eventInstance)) {
return;
}

if (
$eventInstance instanceof WorkspaceWasDiscarded
|| $eventInstance instanceof WorkspaceWasPartiallyDiscarded
Expand All @@ -198,20 +234,17 @@ public function onAfterEvent(EventInterface $eventInstance, EventEnvelope $event
} elseif (
!($eventInstance instanceof NodeAggregateWasRemoved)
&& $eventInstance instanceof EmbedsContentStreamAndNodeAggregateId
// TODO: We need some interface to ensure workspaceName is present
&& property_exists($eventInstance, 'workspaceName')
) {
$workspace = $this->contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId($eventInstance->getContentStreamId());
if ($workspace === null) {
return;
}
// FIXME: EventInterface->workspaceName
$nodeAggregate = $this->contentRepository->getContentGraph($workspace->workspaceName)->findNodeAggregateById(
$nodeAggregate = $this->contentRepository->getContentGraph($eventInstance->workspaceName)->findNodeAggregateById(
$eventInstance->getNodeAggregateId()
);

if ($nodeAggregate) {
$this->scheduleCacheFlushJobForNodeAggregate(
$this->contentRepository,
$workspace->workspaceName,
$eventInstance->workspaceName,
$nodeAggregate
);
}
Expand All @@ -229,7 +262,7 @@ private function scheduleCacheFlushJobForNodeAggregate(
$workspaceName,
$nodeAggregate->nodeAggregateId,
$nodeAggregate->nodeTypeName,
$this->determineParentNodeAggregateIds($workspaceName, $nodeAggregate->nodeAggregateId)
$this->determineParentNodeAggregateIds($workspaceName, $nodeAggregate->nodeAggregateId, NodeAggregateIds::createEmpty())
);
}

Expand All @@ -244,7 +277,7 @@ private function scheduleCacheFlushJobForWorkspaceName(
);
}

private function determineParentNodeAggregateIds(WorkspaceName $workspaceName, NodeAggregateId $childNodeAggregateId): NodeAggregateIds
private function determineParentNodeAggregateIds(WorkspaceName $workspaceName, NodeAggregateId $childNodeAggregateId, NodeAggregateIds $collectedParentNodeAggregateIds): NodeAggregateIds
{
$parentNodeAggregates = $this->contentRepository->getContentGraph($workspaceName)->findParentNodeAggregates($childNodeAggregateId);
$parentNodeAggregateIds = NodeAggregateIds::fromArray(
Expand All @@ -253,12 +286,13 @@ private function determineParentNodeAggregateIds(WorkspaceName $workspaceName, N

foreach ($parentNodeAggregateIds as $parentNodeAggregateId) {
// Prevent infinite loops
if (!$parentNodeAggregateIds->contain($parentNodeAggregateId)) {
$parentNodeAggregateIds->merge($this->determineParentNodeAggregateIds($workspaceName, $parentNodeAggregateId));
if (!$collectedParentNodeAggregateIds->contain($parentNodeAggregateId)) {
$collectedParentNodeAggregateIds = $collectedParentNodeAggregateIds->merge(NodeAggregateIds::create($parentNodeAggregateId));
$collectedParentNodeAggregateIds = $this->determineParentNodeAggregateIds($workspaceName, $parentNodeAggregateId, $collectedParentNodeAggregateIds);
}
}

return $parentNodeAggregateIds;
return $collectedParentNodeAggregateIds;
}

public function onBeforeBatchCompleted(): void
Expand Down
47 changes: 40 additions & 7 deletions Neos.Neos/Tests/Behavior/Features/ContentCache/Assets.feature
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Feature: Tests for the ContentCacheFlusher and cache flushing on asset changes
And the asset "an-asset-to-change" has the title "First asset" and caption "This is an asset" and copyright notice "Copyright Neos 2024"
When an asset exists with id "some-other-asset"
And the asset "some-other-asset" has the title "Some other asset" and caption "This is some other asset" and copyright notice "Copyright Neos 2024"
When an asset exists with id "an-asset-to-change-deep-in-tree"
And the asset "an-asset-to-change-deep-in-tree" has the title "Deep in the tree" and caption "This is an asset, deep in the tree" and copyright notice "Copyright Neos 2024"
And the ContentCacheFlusher flushes all collected tags

When the command CreateRootWorkspace is executed with payload:
Expand All @@ -59,13 +61,14 @@ Feature: Tests for the ContentCacheFlusher and cache flushing on asset changes
| nodeAggregateId | "root" |
| nodeTypeName | "Neos.Neos:Sites" |
And the following CreateNodeAggregateWithNode commands are executed:
| nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName |
| a | root | Neos.Neos:Site | {} | site |
| a1 | a | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1", "title": "Node a1", "asset": "Asset:an-asset-to-change"} | a1 |
| a1-1 | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1-1", "title": "Node a1-1", "assets": ["Asset:an-asset-to-change"]} | a1-1 |
| a1-2 | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1-2", "title": "Node a1-2", "asset": "Asset:some-other-asset"} | a1-2 |
| a2 | a | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a2", "title": "Node a2", "text": "Link to asset://an-asset-to-change."} | a2 |
| a3 | a | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a2", "title": "Node a2", "text": "Link to asset://some-other-asset."} | a3 |
| nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName |
| a | root | Neos.Neos:Site | {} | site |
| a1 | a | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1", "title": "Node a1", "asset": "Asset:an-asset-to-change"} | a1 |
| a1-1 | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1-1", "title": "Node a1-1", "assets": ["Asset:an-asset-to-change"]} | a1-1 |
| a1-2 | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1-2", "title": "Node a1-2", "asset": "Asset:some-other-asset"} | a1-2 |
| a1-1-1 | a1-1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1-1-1", "title": "Node a1-1-1", "assets": ["Asset:an-asset-to-change-deep-in-tree"]} | a1-1-1 |
| a2 | a | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a2", "title": "Node a2", "text": "Link to asset://an-asset-to-change."} | a2 |
| a3 | a | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a2", "title": "Node a2", "text": "Link to asset://some-other-asset."} | a3 |
When the command RebaseWorkspace is executed with payload:
| Key | Value |
| workspaceName | "user-test" |
Expand Down Expand Up @@ -319,3 +322,33 @@ Feature: Tests for the ContentCacheFlusher and cache flushing on asset changes
"""
cacheVerifier=first execution, text=Link to asset://some-other-asset.
"""

Scenario: ContentCache gets flushed when an referenced asset in a property has changed in a descendant node (2 levels)
Given I have Fusion content cache enabled
And the Fusion context node is "a1"

And I execute the following Fusion code:
"""fusion
test = Neos.Neos:Test.DocumentType1 {
cacheVerifier = ${"first execution"}
}
"""
Then I expect the following Fusion rendering result:
"""
cacheVerifier=first execution, assetTitle=First asset, assetTitleOfArray=
"""

Then the asset "an-asset-to-change-deep-in-tree" has the title "Deep in the tree changed"
And the ContentCacheFlusher flushes all collected tags

Then the Fusion context node is "a1"
And I execute the following Fusion code:
"""fusion
test = Neos.Neos:Test.DocumentType1 {
cacheVerifier = ${"second execution"}
}
"""
Then I expect the following Fusion rendering result:
"""
cacheVerifier=second execution, assetTitle=First asset, assetTitleOfArray=
"""
45 changes: 39 additions & 6 deletions Neos.Neos/Tests/Behavior/Features/ContentCache/Nodes.feature
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ Feature: Tests for the ContentCacheFlusher and cache flushing on node and nodety
| nodeAggregateId | "root" |
| nodeTypeName | "Neos.Neos:Sites" |
And the following CreateNodeAggregateWithNode commands are executed:
| nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName |
| a | root | Neos.Neos:Site | {} | site |
| a1 | a | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1", "title": "Node a1"} | a1 |
| a1-1 | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1-1", "title": "Node a1-1"} | a1-1 |
| a2 | a | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a2", "title": "Node a2"} | a2 |
| a3 | a | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a3", "title": "Node a3"} | a3 |
| nodeAggregateId | parentNodeAggregateId | nodeTypeName | initialPropertyValues | nodeName |
| a | root | Neos.Neos:Site | {} | site |
| a1 | a | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1", "title": "Node a1"} | a1 |
| a1-1 | a1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1-1", "title": "Node a1-1"} | a1-1 |
| a1-1-1 | a1-1 | Neos.Neos:Test.DocumentType1 | {"uriPathSegment": "a1-1-1", "title": "Node a1-1-1"} | a1-1-1 |
| a2 | a | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a2", "title": "Node a2"} | a2 |
| a3 | a | Neos.Neos:Test.DocumentType2 | {"uriPathSegment": "a3", "title": "Node a3"} | a3 |
And A site exists for node name "a" and domain "http://localhost"
And the sites configuration is:
"""yaml
Expand Down Expand Up @@ -276,3 +277,35 @@ Feature: Tests for the ContentCacheFlusher and cache flushing on node and nodety
"""
cacheVerifier=second execution, title=Node a1
"""

Scenario: ContentCache gets flushed when a property of a node has changed of a descendant node (2 levels)
Given I have Fusion content cache enabled
And the Fusion context node is "a1"
And I execute the following Fusion code:
"""fusion
test = Neos.Neos:Test.DocumentType1 {
cacheVerifier = ${"first execution"}
}
"""
Then I expect the following Fusion rendering result:
"""
cacheVerifier=first execution, title=Node a1
"""

When the command SetNodeProperties is executed with payload:
| Key | Value |
| contentStreamId | "cs-identifier" |
| nodeAggregateId | "a1-1-1" |
| propertyValues | {"title": "Node a1-1-1 new"} |

And the Fusion context node is "a1"
And I execute the following Fusion code:
"""fusion
test = Neos.Neos:Test.DocumentType1 {
cacheVerifier = ${"second execution"}
}
"""
Then I expect the following Fusion rendering result:
"""
cacheVerifier=second execution, title=Node a1
"""
Loading

0 comments on commit e2bc811

Please sign in to comment.