Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUGFIX: Flush node caches when asset has changed #4788

Merged
merged 16 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
163 changes: 90 additions & 73 deletions Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@
use Neos\ContentRepository\Core\SharedModel\Exception\NodeTypeNotFoundException;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Persistence\PersistenceManagerInterface;
use Neos\Fusion\Core\Cache\ContentCache;
use Neos\Media\Domain\Model\AssetInterface;
use Neos\Media\Domain\Model\AssetVariantInterface;
use Neos\Neos\AssetUsage\Dto\AssetUsageFilter;
use Neos\Neos\AssetUsage\GlobalAssetUsageService;
use Psr\Log\LoggerInterface;

/**
Expand All @@ -36,17 +40,24 @@
* This is the relevant case if publishing a workspace
* - where we f.e. need to flush the cache for Live.
*
* @Flow\Scope("singleton")
*/
#[Flow\Scope('singleton')]
class ContentCacheFlusher
{
#[Flow\InjectConfiguration(path: "fusion.contentCacheDebugMode")]
protected bool $debugMode;

/**
* @var array<string,string>
*/
private array $tagsToFlushAfterPersistance = [];

public function __construct(
protected ContentCache $contentCache,
protected LoggerInterface $systemLogger,
protected readonly ContentCache $contentCache,
protected readonly LoggerInterface $systemLogger,
protected readonly GlobalAssetUsageService $globalAssetUsageService,
protected readonly ContentRepositoryRegistry $contentRepositoryRegistry,
protected readonly PersistenceManagerInterface $persistenceManager,
) {
}

Expand All @@ -62,28 +73,41 @@ public function flushNodeAggregate(
ContentStreamId $contentStreamId,
NodeAggregateId $nodeAggregateId
): void {
$tagsToFlush = [];

$tagsToFlush[ContentCache::TAG_EVERYTHING] = 'which were tagged with "Everything".';

$this->registerChangeOnNodeIdentifier($contentRepository->id, $contentStreamId, $nodeAggregateId, $tagsToFlush);
$tagsToFlush = array_merge(
$this->collectTagsForChangeOnNodeAggregate($contentRepository, $contentStreamId, $nodeAggregateId),
$tagsToFlush
);

$this->flushTags($tagsToFlush);
}

/**
* @return array<string,string>
*/
private function collectTagsForChangeOnNodeAggregate(
ContentRepository $contentRepository,
ContentStreamId $contentStreamId,
NodeAggregateId $nodeAggregateId
): array {
$nodeAggregate = $contentRepository->getContentGraph()->findNodeAggregateById(
$contentStreamId,
$nodeAggregateId
);
if (!$nodeAggregate) {
// Node Aggregate was removed in the meantime, so no need to clear caches on this one anymore.
return;
return [];
}
$tagsToFlush = $this->collectTagsForChangeOnNodeIdentifier($contentRepository->id, $contentStreamId, $nodeAggregateId);

$this->registerChangeOnNodeType(
$tagsToFlush = array_merge($this->collectTagsForChangeOnNodeType(
$nodeAggregate->nodeTypeName,
$contentRepository->id,
$contentStreamId,
$nodeAggregateId,
$tagsToFlush,
$contentRepository
);
), $tagsToFlush);

$parentNodeAggregates = [];
foreach (
Expand Down Expand Up @@ -130,54 +154,55 @@ public function flushNodeAggregate(
$parentNodeAggregates[] = $parentNodeAggregate;
}
}
$this->flushTags($tagsToFlush);

return $tagsToFlush;
}


/**
* Please 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 array<string,string> &$tagsToFlush
* @return array<string, string>
*/
private function registerChangeOnNodeIdentifier(
private function collectTagsForChangeOnNodeIdentifier(
ContentRepositoryId $contentRepositoryId,
ContentStreamId $contentStreamId,
NodeAggregateId $nodeAggregateId,
array &$tagsToFlush
): void {
): array {
$tagsToFlush = [];

$nodeCacheIdentifier = CacheTag::forNodeAggregate($contentRepositoryId, $contentStreamId, $nodeAggregateId);
$tagsToFlush[$nodeCacheIdentifier->value] = sprintf(
'which were tagged with "%s" because that identifier has changed.',
$nodeCacheIdentifier->value
);

$descandantOfNodeCacheIdentifier = CacheTag::forDescendantOfNode($contentRepositoryId, $contentStreamId, $nodeAggregateId);
$tagsToFlush[$descandantOfNodeCacheIdentifier->value] = sprintf(
$dynamicNodeCacheIdentifier = CacheTag::forDynamicNodeAggregate($contentRepositoryId, $contentStreamId, $nodeAggregateId);
$tagsToFlush[$dynamicNodeCacheIdentifier->value] = sprintf(
'which were tagged with "%s" because that identifier has changed.',
$dynamicNodeCacheIdentifier->value
);

$descendantOfNodeCacheIdentifier = CacheTag::forDescendantOfNode($contentRepositoryId, $contentStreamId, $nodeAggregateId);
$tagsToFlush[$descendantOfNodeCacheIdentifier->value] = sprintf(
'which were tagged with "%s" because node "%s" has changed.',
$descandantOfNodeCacheIdentifier->value,
$descendantOfNodeCacheIdentifier->value,
$nodeCacheIdentifier->value
);

return $tagsToFlush;
}

/**
* 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 array<string,string> &$tagsToFlush
* @return array<string,string> $tagsToFlush
*/
private function registerChangeOnNodeType(
private function collectTagsForChangeOnNodeType(
NodeTypeName $nodeTypeName,
ContentRepositoryId $contentRepositoryId,
ContentStreamId $contentStreamId,
?NodeAggregateId $referenceNodeIdentifier,
array &$tagsToFlush,
ContentRepository $contentRepository
): void {
): array {
$tagsToFlush = [];

$nodeType = $contentRepository->getNodeTypeManager()->getNodeType($nodeTypeName);
if ($nodeType) {
$nodeTypesNamesToFlush = $this->getAllImplementedNodeTypeNames($nodeType);
Expand All @@ -195,11 +220,13 @@ private function registerChangeOnNodeType(
$nodeTypeName->value
);
}

return $tagsToFlush;
}


/**
* Flush caches according to the previously registered node changes.
* Flush caches according to the given tags.
*
* @param array<string,string> $tagsToFlush
*/
Expand Down Expand Up @@ -250,55 +277,45 @@ 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
// no flushing of corresponding entries happens. Thus we instead us the original asset
// no flushing of corresponding entries happens. Thus we instead use the original asset
// of the variant.
if ($asset instanceof AssetVariantInterface) {
$asset = $asset->getOriginalAsset();
}

// TODO: re-implement this based on the code below

/*
if (!$asset->isInUse()) {
return;
$tagsToFlush = [];
$filter = AssetUsageFilter::create()
->withAsset($this->persistenceManager->getIdentifierByObject($asset))
->includeVariantsOfAsset();

foreach ($this->globalAssetUsageService->findByFilter($filter) as $contentRepositoryId => $usages) {
foreach ($usages as $usage) {
$contentRepository = $this->contentRepositoryRegistry->get(ContentRepositoryId::fromString($contentRepositoryId));
$tagsToFlush = array_merge(
$this->collectTagsForChangeOnNodeAggregate(
$contentRepository,
$usage->contentStreamId,
$usage->nodeAggregateId
),
$tagsToFlush
);
}
}

$cachingHelper = $this->getCachingHelper();

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

$workspaceHash = $cachingHelper->renderWorkspaceTagForContextNode($reference->getWorkspaceName());
$this->securityContext->withoutAuthorizationChecks(function () use ($reference, &$node) {
$node = $this->getContextForReference($reference)->getNodeByIdentifier($reference->getNodeIdentifier());
});

if (!$node instanceof Node) {
$this->systemLogger->warning(sprintf(
'Found a node reference from node with identifier %s in workspace %s to asset %s,'
. ' but the node could not be fetched.',
$reference->getNodeIdentifier(),
$reference->getWorkspaceName(),
$this->persistenceManager->getIdentifierByObject($asset)
), LogEnvironment::fromMethodName(__METHOD__));
continue;
}
/**
* Flush caches according to the previously registered changes.
*/
public function flushCollectedTags(): void
{
$this->flushTags($this->tagsToFlushAfterPersistance);
$this->tagsToFlushAfterPersistance = [];
}

$this->registerNodeChange($node);

$assetIdentifier = $this->persistenceManager->getIdentifierByObject($asset);
// @see RuntimeContentCache.addTag
$tagName = 'AssetDynamicTag_' . $workspaceHash . '_' . $assetIdentifier;
$this->addTagToFlush(
$tagName,
sprintf(
'which were tagged with "%s" because asset "%s" has changed.',
$tagName,
$assetIdentifier
)
);
}*/
public function shutdownObject(): void
{
$this->flushCollectedTags();
}
}
22 changes: 20 additions & 2 deletions Neos.Neos/Tests/Behavior/Features/Bootstrap/AssetTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
/**
* Step implementations for tests inside Neos.Neos
*
* @internal only for behat tests within the Neos.Neos package
*/
* @internal only for behat tests within the Neos.Neos package
*/
trait AssetTrait
{
/**
Expand All @@ -46,4 +46,22 @@ public function anAssetExistsWithId(string $assetId): void
$this->getObject(AssetRepository::class)->add($asset);
$this->getObject(PersistenceManagerInterface::class)->persistAll();
}

/**
* @Given the asset :assetId has the title :title
* @Given the asset :assetId has the title :title and caption :caption
* @Given the asset :assetId has the title :title and caption :caption and copyright notice :copyrightNotice
*/
public function theAssetHasTheTitleAndCaptionAndCopyrightNotice($assetId, $title, $caption = null, $copyrightNotice = null): void
{
$repository = $this->getObject(AssetRepository::class);
$asset = $repository->findByIdentifier($assetId);

$asset->setTitle($title);
$caption && $asset->setCaption($caption);
$copyrightNotice && $asset->setCopyrightNotice($copyrightNotice);

$repository->update($asset);
$this->getObject(PersistenceManagerInterface::class)->persistAll();
}
}
39 changes: 39 additions & 0 deletions Neos.Neos/Tests/Behavior/Features/Bootstrap/ContentCacheTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);

/*
* This file is part of the Neos.ContentRepository package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\Neos\Fusion\Cache\ContentCacheFlusher;

/**
* Step implementations for tests inside Neos.Neos
*
* @internal only for behat tests within the Neos.Neos package
*/
trait ContentCacheTrait
{
/**
* @template T of object
* @param class-string<T> $className
*
* @return T
*/
abstract private function getObject(string $className): object;


/**
* @Given the ContentCacheFlusher flushes all collected tags
*/
public function theContentCacheFlusherFlushesAllCollectedTags(): void
{
$this->getObject(ContentCacheFlusher::class)->flushCollectedTags();
}
}