Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: CI

on:
push:
branches: [ main ]
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
phpunit:
name: PHPUnit (PHP ${{ matrix.php_version }})
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
php_version: [ '8.1', '8.5' ]

steps:
- uses: actions/checkout@v7
- uses: php-actions/composer@v6
with:
php_version: ${{ matrix.php_version }}
- name: PHPUnit Tests
uses: php-actions/phpunit@v4
with:
php_version: ${{ matrix.php_version }}
php_extensions: xdebug
bootstrap: vendor/autoload.php
configuration: phpunit.xml
coverage_text: true
env:
XDEBUG_MODE: coverage

phpstan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: php-actions/composer@v6
with:
php_version: '8.1'
- uses: php-actions/phpstan@v3
with:
php_version: '8.1'
configuration: phpstan.neon
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
node_modules
composer.lock
vendor
Packages
.phpunit.cache
.phpunit.result.cache
69 changes: 55 additions & 14 deletions Classes/Aspects/CacheUrlMappingAspect.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@

namespace Flowpack\DecoupledContentStore\Aspects;

use Flowpack\DecoupledContentStore\Exception;
use Flowpack\DecoupledContentStore\Core\Infrastructure\ContentReleaseLogger;
use Flowpack\DecoupledContentStore\Exception;
use Flowpack\DecoupledContentStore\NodeRendering\Dto\DocumentNodeCacheKey;
use Flowpack\DecoupledContentStore\NodeRendering\Dto\DocumentNodeCacheValues;
use Flowpack\DecoupledContentStore\NodeRendering\Extensibility\NodeRenderingExtensionManager;
use Flowpack\DecoupledContentStore\NodeRendering\Render\DocumentRenderer;
use Flowpack\DecoupledContentStore\NodeRendering\Render\RenderExceptionExtractor;
use Neos\Cache\Exception\InvalidDataException;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Aop\JoinPointInterface;
use Neos\Fusion\Core\Cache\CacheSegmentParser;
use Neos\Utility\Exception\PropertyNotAccessibleException;
use Neos\Utility\ObjectAccess;
use Neos\ContentRepository\Domain\Model\NodeInterface;

/**
* This aspect creates the root cache entry which maps the URL to the root cache identifier during rendering.
Expand Down Expand Up @@ -102,13 +104,22 @@ public function getCurrentEvaluateAndControllerContext(JoinPointInterface $joinP

/**
* @Flow\After("method(Neos\Fusion\Core\Cache\ContentCache->processCacheSegments())")
* @throws Exception
* @throws \JsonException
* @throws \Neos\Cache\Exception
* @throws InvalidDataException
* @throws \Neos\Fusion\Exception
* @throws PropertyNotAccessibleException
*/
public function storeRootCacheIdentifier(JoinPointInterface $joinPoint)
public function storeRootCacheIdentifier(JoinPointInterface $joinPoint): void
{
if (!$this->isActive) {
return;
}
if (!isset($this->currentEvaluateContext['cacheIdentifierValues']['node']) || !$this->currentEvaluateContext['cacheIdentifierValues']['node'] instanceof NodeInterface) {
if (
!isset($this->currentEvaluateContext['cacheIdentifierValues']['node'])
|| !$this->currentEvaluateContext['cacheIdentifierValues']['node'] instanceof NodeInterface
) {
return;
}

Expand All @@ -122,7 +133,17 @@ public function storeRootCacheIdentifier(JoinPointInterface $joinPoint)
if (!$storeCacheEntries) {
$content = $joinPoint->getMethodArgument('content');
$extractedExceptionDto = RenderExceptionExtractor::extractRenderingException($content);
throw new Exception('Cache was disabled for ' . $url . ' with node ' . $node->getContextPath() . ', but no exception was handled by the publishing. This could be caused by a missing publishing aware @exceptionHandler in Fusion.' . ($extractedExceptionDto !== null ? "\nException extracted from output: {$extractedExceptionDto}" : ''), 1539156004);
throw new Exception(
'Cache was disabled for '
. $url
. ' with node '
. $node->getContextPath()
. ', but no exception was handled by the publishing. This could be caused by a missing publishing aware @exceptionHandler in Fusion.'
. (
$extractedExceptionDto !== null ? "\nException extracted from output: {$extractedExceptionDto}" : ''
),
1539156004
);
}

$content = $joinPoint->getMethodArgument('content');
Expand All @@ -141,25 +162,43 @@ public function storeRootCacheIdentifier(JoinPointInterface $joinPoint)
throw new \RuntimeException('TODO Logger not found - should never happen');
}
if ($this->urlIsMatchingBlacklist($url)) {
$logger->info(sprintf('Skipping URL %s, because it matches the blacklist %s', $url, $this->urlExcludelistRegex));
$logger->info(sprintf(
'Skipping URL %s, because it matches the blacklist %s',
$url,
$this->urlExcludelistRegex
));

return;
}

if ($rootIdentifier === null) {
throw new Exception('Could not find root cache identifier for ' . $url . ', possible rendering error?', 1491394849);
throw new Exception(
'Could not find root cache identifier for ' . $url . ', possible rendering error?',
1491394849
);
}

$logger->debug('Mapping URL ' . $url . ' to ' . $rootIdentifier . ' with tags ' . implode(', ', $rootTags));

$arguments = $this->getCurrentArguments($node);
// TODO: To make parallel rendering possible, we need to make sure that the cache key also includes the currently rendered workspace, as the node might originate from a base workspace (usually live). See `DocumentNodeCacheKey`.
$rootKey = DocumentNodeCacheKey::fromNodeAndArguments($node, $arguments);
$rootCacheValues = DocumentNodeCacheValues::create($rootIdentifier, $url)
->withMetadata('renderTime', (int)(microtime(true) * 1000) - $this->renderTimestamp);
$rootCacheValues = DocumentNodeCacheValues::create($rootIdentifier, $url)->withMetadata(
'renderTime',
(int) ( microtime(true) * 1000 ) - $this->renderTimestamp
);
// allow other document metadata generators here
$rootCacheValues = $this->nodeRenderingExtensionManager->runDocumentMetadataGenerators($node, $arguments, $this->controllerContext, $rootCacheValues);
$this->contentCacheFrontend->set($rootKey->redisKeyName(), json_encode($rootCacheValues), $rootTags);
$rootCacheValues = $this->nodeRenderingExtensionManager->runDocumentMetadataGenerators(
$node,
$arguments,
$this->controllerContext,
$rootCacheValues
);
$this->contentCacheFrontend->set(
$rootKey->redisKeyName(),
json_encode($rootCacheValues, JSON_THROW_ON_ERROR),
$rootTags
);
$this->mappingWasWrittenForCurrentDocument = true;
}

Expand All @@ -174,7 +213,7 @@ protected function getCurrentUrl(): string
$url = $httpRequest->getUri();
$url = $url->withQuery('');

return (string)$url;
return (string) $url;
}

/**
Expand Down Expand Up @@ -214,7 +253,7 @@ public function beforeDocumentRendering(ContentReleaseLogger $contentReleaseLogg
{
$this->isActive = true;
$this->contentReleaseLogger = $contentReleaseLogger;
$this->renderTimestamp = (int)(microtime(true) * 1000);
$this->renderTimestamp = (int) ( microtime(true) * 1000 );
$this->mappingWasWrittenForCurrentDocument = false;
}

Expand All @@ -225,7 +264,9 @@ public function afterDocumentRendering(): void
// error), so we make some noise - otherwise the content release just fails with the retry limit and no hint
// about the reason. {@see storeRootCacheIdentifier()}
if (!$this->mappingWasWrittenForCurrentDocument && $this->contentReleaseLogger !== null) {
$this->contentReleaseLogger->warn('No "doc--..." mapping entry was written for this rendering, so it can never be added to the content release. Either the rendering was fully served from the content cache (then the content cache entries of this node need to be flushed before re-rendering), or its URL is excluded via nodeRendering.urlExcludelistRegex while the node is still part of the enumeration.');
$this->contentReleaseLogger->warn(
'No "doc--..." mapping entry was written for this rendering, so it can never be added to the content release. Either the rendering was fully served from the content cache (then the content cache entries of this node need to be flushed before re-rendering), or its URL is excluded via nodeRendering.urlExcludelistRegex while the node is still part of the enumeration.'
);
}

$this->isActive = false;
Expand Down
31 changes: 20 additions & 11 deletions Classes/Aspects/FixedAssetHandlingInContentCacheFlusherAspect.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace Flowpack\DecoupledContentStore\Aspects;

use Flowpack\DecoupledContentStore\ContentReleaseManager;
Expand Down Expand Up @@ -107,36 +109,43 @@ public function registerAssetChange(JoinPointInterface $joinPoint)
// 1. flush asset tag without workspace hash
$assetIdentifier = $this->persistenceManager->getIdentifierByObject($asset);

$assetCacheTag = "AssetDynamicTag_" . $assetIdentifier;
$assetCacheTag = 'AssetDynamicTag_' . $assetIdentifier;

// WHY: ContentCacheFlusher has no public api to flush tags directly
$tagsToFlush = ObjectAccess::getProperty($contentCacheFlusher, 'tagsToFlush', true);
$tagsToFlush[$assetCacheTag] = sprintf('which were tagged with "%s" because asset "%s" has changed.', $assetCacheTag, $assetIdentifier);
$tagsToFlush[$assetCacheTag] = sprintf(
'which were tagged with "%s" because asset "%s" has changed.',
$assetCacheTag,
$assetIdentifier
);
ObjectAccess::setProperty($contentCacheFlusher, 'tagsToFlush', $tagsToFlush, true);

$usageReferences = $this->assetService->getUsageReferences($asset);

foreach ($usageReferences as $assetUsage) {
// get node that uses the asset
$context = $this->_contextFactory->create(
[
'workspaceName' => $assetUsage->getWorkspaceName(),
'dimensions' => $assetUsage->getDimensionValues(),
'invisibleContentShown' => true,
'removedContentShown' => true]
);
$context = $this->_contextFactory->create([
'workspaceName' => $assetUsage->getWorkspaceName(),
'dimensions' => $assetUsage->getDimensionValues(),
'invisibleContentShown' => true,
'removedContentShown' => true
]);

$node = $context->getNodeByIdentifier($assetUsage->getNodeIdentifier());

// We need this for cache tag generation
$workspaceHash = $this->cachingHelper->renderWorkspaceTagForContextNode($context->getWorkspaceName());

// 1. flush asset with workspace hash
$assetCacheTagWithWorkspace = "AssetDynamicTag_" . $workspaceHash . "_" . $assetIdentifier;
$assetCacheTagWithWorkspace = 'AssetDynamicTag_' . $workspaceHash . '_' . $assetIdentifier;

// WHY: ContentCacheFlusher has no public api to flush tags directly
$tagsToFlush = ObjectAccess::getProperty($contentCacheFlusher, 'tagsToFlush', true);
$tagsToFlush[$assetCacheTagWithWorkspace] = sprintf('which were tagged with "%s" because asset "%s" has changed.', $assetCacheTagWithWorkspace, $assetIdentifier);
$tagsToFlush[$assetCacheTagWithWorkspace] = sprintf(
'which were tagged with "%s" because asset "%s" has changed.',
$assetCacheTagWithWorkspace,
$assetIdentifier
);
ObjectAccess::setProperty($contentCacheFlusher, 'tagsToFlush', $tagsToFlush, true);

// 2. flush all nodes on path to parent document node (a bit excessive, but for now it works)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace Flowpack\DecoupledContentStore\Aspects;

use Neos\Flow\Annotations as Flow;
Expand All @@ -8,7 +10,6 @@
use Neos\Neos\Fusion\NodeUriImplementation;
use Neos\Utility\ObjectAccess;


/**
* The {@see ContentCacheFlusher::registerNodeChange()} has one (general-case) bug related to Nodes:
*
Expand All @@ -27,7 +28,6 @@
*/
class FixedNodeLinkHandlingInContentCacheFlusherAspect
{

/**
* @Flow\After("method(Neos\Neos\Fusion\Cache\ContentCacheFlusher->registerNodeChange())")
*/
Expand All @@ -37,7 +37,11 @@ public function registerNodeChange(JoinPointInterface $joinPoint)
$tagName = 'NodeDynamicTag_' . $node->getIdentifier();
$contentCacheFlusher = $joinPoint->getProxy();
$tagsToFlush = ObjectAccess::getProperty($contentCacheFlusher, 'tagsToFlush', true);
$tagsToFlush[$tagName] = sprintf('which were tagged with "%s" because node "%s" has changed.', $tagName, $node->getIdentifier());
$tagsToFlush[$tagName] = sprintf(
'which were tagged with "%s" because node "%s" has changed.',
$tagName,
$node->getIdentifier()
);
ObjectAccess::setProperty($contentCacheFlusher, 'tagsToFlush', $tagsToFlush, true);
}
}
Loading