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 23, 2024
1 parent a9261f4 commit 5c7b240
Show file tree
Hide file tree
Showing 11 changed files with 339 additions and 137 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
use Neos\EventStore\Model\Event\SequenceNumber;
use Neos\EventStore\Model\EventEnvelope;
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Event\WorkspaceWasDiscarded;
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Event\WorkspaceWasPartiallyDiscarded;

/**
* @implements ProjectionInterface<ContentGraphFinder>
Expand Down Expand Up @@ -171,6 +173,8 @@ public function canHandle(EventInterface $event): bool
RootNodeAggregateWithNodeWasCreated::class,
SubtreeWasTagged::class,
SubtreeWasUntagged::class,
WorkspaceWasDiscarded::class,
WorkspaceWasPartiallyDiscarded::class
]);
}

Expand All @@ -195,6 +199,8 @@ public function apply(EventInterface $event, EventEnvelope $eventEnvelope): void
RootNodeAggregateWithNodeWasCreated::class => $this->whenRootNodeAggregateWithNodeWasCreated($event, $eventEnvelope),
SubtreeWasTagged::class => $this->whenSubtreeWasTagged($event),
SubtreeWasUntagged::class => $this->whenSubtreeWasUntagged($event),
WorkspaceWasDiscarded::class => null, // only needed for GraphProjectorCatchUpHookForCacheFlushing
WorkspaceWasPartiallyDiscarded::class => null, // only needed for GraphProjectorCatchUpHookForCacheFlushing
default => throw new \InvalidArgumentException(sprintf('Unsupported event %s', get_debug_type($event))),
};
}
Expand Down
11 changes: 11 additions & 0 deletions Neos.Neos/Classes/Fusion/Cache/CacheTag.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class CacheTag
protected const PREFIX_ANCESTOR = 'Ancestor';
protected const PREFIX_NODE_TYPE = 'NodeType';
protected const PREFIX_DYNAMIC_NODE_TAG = 'DynamicNodeTag';
protected const PREFIX_WORKSPACE = 'Workspace';

private function __construct(
public readonly string $value
Expand Down Expand Up @@ -127,6 +128,16 @@ final public static function forDynamicNodeAggregate(
);
}

final public static function forWorkspaceName(
ContentRepositoryId $contentRepositoryId,
WorkspaceName $workspaceName)
{
return self::fromSegments(
self::PREFIX_WORKSPACE,
self::getHashForWorkspaceNameAndContentRepositoryId($workspaceName, $contentRepositoryId),
);
}

final public static function fromString(string $string): self
{
return new self($string);
Expand Down
20 changes: 17 additions & 3 deletions Neos.Neos/Classes/Fusion/Cache/CacheTagSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ public static function forNodeTypeNames(
));
}

public static function forWorkspaceNameFromNodes(Nodes $nodes): self
{
return new self(...array_map(
static fn (Node $node): CacheTag => CacheTag::forWorkspaceName(
$node->contentRepositoryId,
$node->workspaceName,
),
iterator_to_array($nodes)
));

}

public function add(CacheTag $cacheTag): self
{
$tags = $this->tags;
Expand All @@ -106,9 +118,11 @@ public function add(CacheTag $cacheTag): self
*/
public function toStringArray(): array
{
return array_map(
static fn (CacheTag $tag): string => $tag->value,
array_values($this->tags)
return array_unique(
array_map(
static fn (CacheTag $tag): string => $tag->value,
array_values($this->tags)
)
);
}

Expand Down
107 changes: 42 additions & 65 deletions Neos.Neos/Classes/Fusion/Cache/ContentCacheFlusher.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use Neos\Neos\AssetUsage\Dto\AssetUsageFilter;
use Neos\Neos\AssetUsage\GlobalAssetUsageService;
use Psr\Log\LoggerInterface;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds;

/**
* This service flushes Fusion content caches triggered by node changes.
Expand Down Expand Up @@ -61,18 +62,33 @@ public function __construct(
) {
}

/**
* Main entry point to *directly* flush the caches of a given workspaceName
*/
public function flushWorkspace(
FlushWorkspaceRequest $flushWorkspaceRequest
): void {
$tagsToFlush[ContentCache::TAG_EVERYTHING] = 'which were tagged with "Everything".';

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

$this->flushTags($tagsToFlush);
}

/**
* Main entry point to *directly* flush the caches of a given NodeAggregate
*/
public function flushNodeAggregate(
ContentRepository $contentRepository,
WorkspaceName $workspaceName,
NodeAggregateId $nodeAggregateId
FlushNodeAggregateRequest $flushNodeAggregateRequest
): void {
$tagsToFlush[ContentCache::TAG_EVERYTHING] = 'which were tagged with "Everything".';

$tagsToFlush = array_merge(
$this->collectTagsForChangeOnNodeAggregate($contentRepository, $workspaceName, $nodeAggregateId, false),
$this->collectTagsForChangeOnNodeAggregate($flushNodeAggregateRequest, false),
$tagsToFlush
);

Expand All @@ -86,73 +102,28 @@ public function flushNodeAggregate(
* @return array<string,string>
*/
private function collectTagsForChangeOnNodeAggregate(
ContentRepository $contentRepository,
WorkspaceName $workspaceName,
NodeAggregateId $nodeAggregateId,
FlushNodeAggregateRequest $flushNodeAggregateRequest,
bool $anyWorkspace,
): array {
$contentGraph = $contentRepository->getContentGraph($workspaceName);

$nodeAggregate = $contentGraph->findNodeAggregateById(
$nodeAggregateId
);
if (!$nodeAggregate) {
// Node Aggregate was removed in the meantime, so no need to clear caches on this one anymore.
return [];
}
$workspaceNameToFlush = $anyWorkspace ? CacheTagWorkspaceName::ANY : $workspaceName;
$tagsToFlush = $this->collectTagsForChangeOnNodeIdentifier($contentRepository->id, $workspaceNameToFlush, $nodeAggregateId);
$workspaceNameToFlush = $anyWorkspace ? CacheTagWorkspaceName::ANY : $flushNodeAggregateRequest->workspaceName;
$tagsToFlush = $this->collectTagsForChangeOnNodeIdentifier($flushNodeAggregateRequest->contentRepositoryId, $workspaceNameToFlush, $flushNodeAggregateRequest->nodeAggregateId);

$tagsToFlush = array_merge($this->collectTagsForChangeOnNodeType(
$nodeAggregate->nodeTypeName,
$contentRepository->id,
$flushNodeAggregateRequest->nodeTypeName,
$flushNodeAggregateRequest->contentRepositoryId,
$workspaceNameToFlush,
$nodeAggregateId,
$contentRepository
$flushNodeAggregateRequest->nodeAggregateId,
), $tagsToFlush);

$parentNodeAggregates = [];
foreach (
$contentGraph->findParentNodeAggregates(
$nodeAggregateId
) as $parentNodeAggregate
) {
$parentNodeAggregates[] = $parentNodeAggregate;
}
// we do not need these variables anymore here
unset($nodeAggregateId);


// NOTE: Normally, the content graph cannot contain cycles. However, during the
// testcase "Features/ProjectionIntegrityViolationDetection/AllNodesAreConnectedToARootNodePerSubgraph.feature"
// and in case of bugs, it could have actually cycles.
// We still want the content cache flushing to work, without hanging up in an endless loop.
// That's why we track the seen NodeAggregateIds to be sure we don't travers them multiple times.
$processedNodeAggregateIds = [];

while ($nodeAggregate = array_shift($parentNodeAggregates)) {
assert($nodeAggregate instanceof NodeAggregate);
if (isset($processedNodeAggregateIds[$nodeAggregate->nodeAggregateId->value])) {
// we've already processed this NodeAggregateId (i.e. flushed the caches for it);
// thus we can skip this one, and thus break the cycle.
continue;
}
$processedNodeAggregateIds[$nodeAggregate->nodeAggregateId->value] = true;
$parentNodeAggregateIds = $flushNodeAggregateRequest->parentNodeAggregateIds;
foreach ($parentNodeAggregateIds as $parentNodeAggregateId) {

$tagName = CacheTag::forDescendantOfNode($contentRepository->id, $workspaceName, $nodeAggregate->nodeAggregateId);
$tagName = CacheTag::forDescendantOfNode($flushNodeAggregateRequest->contentRepositoryId, $workspaceNameToFlush, $parentNodeAggregateId);
$tagsToFlush[$tagName->value] = sprintf(
'which were tagged with "%s" because node "%s" has changed.',
$tagName->value,
$nodeAggregate->nodeAggregateId->value
$parentNodeAggregateId->value
);

foreach (
$contentGraph->findParentNodeAggregates(
$nodeAggregate->nodeAggregateId
) as $parentNodeAggregate
) {
$parentNodeAggregates[] = $parentNodeAggregate;
}
}

return $tagsToFlush;
Expand Down Expand Up @@ -198,11 +169,11 @@ private function collectTagsForChangeOnNodeType(
NodeTypeName $nodeTypeName,
ContentRepositoryId $contentRepositoryId,
WorkspaceName|CacheTagWorkspaceName $workspaceName,
?NodeAggregateId $referenceNodeIdentifier,
ContentRepository $contentRepository
?NodeAggregateId $referenceNodeIdentifier
): array {
$tagsToFlush = [];

$contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId);
$nodeType = $contentRepository->getNodeTypeManager()->getNodeType($nodeTypeName);
if ($nodeType) {
$nodeTypesNamesToFlush = $this->getAllImplementedNodeTypeNames($nodeType);
Expand Down Expand Up @@ -273,6 +244,7 @@ function (array $types, NodeType $superType) use ($self) {
*
* @throws NodeTypeNotFound
*/
// TODO: Move out of the ContentCacheFlusher and
public function registerAssetChange(AssetInterface $asset): void
{
// In Nodes only assets are referenced, never asset variants directly. When an asset
Expand Down Expand Up @@ -305,12 +277,17 @@ public function registerAssetChange(AssetInterface $asset): void
}
//

$contentRepository = $this->contentRepositoryRegistry->get(ContentRepositoryId::fromString($contentRepositoryId));
$flushNodeAggregateRequest = FlushNodeAggregateRequest::create(
ContentRepositoryId::fromString($contentRepositoryId),
$workspaceName,
$usage->nodeAggregateId,
NodeTypeName::fromString("Neos.Neos:Content"),
NodeAggregateIds::create(),
);

$tagsToFlush = array_merge(
$this->collectTagsForChangeOnNodeAggregate(
$contentRepository,
$workspaceName,
$usage->nodeAggregateId,
$flushNodeAggregateRequest,
true
),
$tagsToFlush
Expand Down
38 changes: 38 additions & 0 deletions Neos.Neos/Classes/Fusion/Cache/FlushNodeAggregateRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Neos\Neos\Fusion\Cache;

use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds;
use Neos\ContentRepository\Core\NodeType\NodeTypeName;
use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;

final readonly class FlushNodeAggregateRequest
{
private function __construct(
public ContentRepositoryId $contentRepositoryId,
public WorkspaceName $workspaceName,
public NodeAggregateId $nodeAggregateId,
public NodeTypeName $nodeTypeName,
public NodeAggregateIds $parentNodeAggregateIds,
) {

}

public static function create(
ContentRepositoryId $contentRepositoryId,
WorkspaceName $workspaceName,
NodeAggregateId $nodeAggregateId,
NodeTypeName $nodeTypeName,
NodeAggregateIds $parentNodeAggregateIds
): self {
return new self($contentRepositoryId,
$workspaceName,
$nodeAggregateId,
$nodeTypeName,
$parentNodeAggregateIds
);
}
}
25 changes: 25 additions & 0 deletions Neos.Neos/Classes/Fusion/Cache/FlushWorkspaceRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Neos\Neos\Fusion\Cache;

use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId;
use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateIds;
use Neos\ContentRepository\Core\NodeType\NodeTypeName;
use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;

final readonly class FlushWorkspaceRequest
{
private function __construct(
public ContentRepositoryId $contentRepositoryId,
public WorkspaceName $workspaceName,
) {

}

public static function create(ContentRepositoryId $contentRepositoryId, WorkspaceName $workspaceName): self
{
return new self($contentRepositoryId, $workspaceName);
}
}
Loading

0 comments on commit 5c7b240

Please sign in to comment.