Skip to content

Commit db29a0a

Browse files
authored
BUGFIX: Fix asset cache flush after asset update (#37)
1 parent 6adb8e0 commit db29a0a

3 files changed

Lines changed: 121 additions & 7 deletions

File tree

Classes/Aspects/FixedAssetHandlingInContentCacheFlusherAspect.php

Lines changed: 109 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22

33
namespace Flowpack\DecoupledContentStore\Aspects;
44

5+
use Flowpack\DecoupledContentStore\ContentReleaseManager;
56
use Neos\Flow\Annotations as Flow;
67
use Neos\Flow\Aop\JoinPointInterface;
78
use Neos\Flow\Persistence\PersistenceManagerInterface;
9+
use Neos\Media\Domain\Model\AssetInterface;
10+
use Neos\Media\Domain\Service\AssetService;
11+
use Neos\Neos\Controller\CreateContentContextTrait;
12+
use Neos\Neos\Fusion\Cache\ContentCacheFlusher;
13+
use Neos\Neos\Fusion\Helper\CachingHelper;
814
use Neos\Utility\ObjectAccess;
915

10-
1116
/**
1217
* The ContentCacheFlusher::registerAssetChange() has one (general-case) bug, which we patch here.
1318
*
@@ -27,40 +32,137 @@
2732
* by the line `documentRendering.@cache.entryTags.2 >`. It is also mentioned here for completeness; so that one has a
2833
* complete overview of the full issue.
2934
*
35+
* 3) Additional Bug: The default implementation of ContentCacheFlusher::registerAssetChange() does not flush the cache of the parent
36+
* nodes until and including the parent document node of all nodes that use that asset. It only flushes the cache of the
37+
* node that uses the asset. This is changed here.
38+
*
39+
* 4) Note: We also directly commit the cache flush and trigger the incremental rendering afterwards to prevent a race condition with
40+
* node enumeration during incremental rendering.
41+
*
3042
* @Flow\Aspect
3143
* @Flow\Scope("singleton")
3244
*/
3345
class FixedAssetHandlingInContentCacheFlusherAspect
3446
{
47+
use CreateContentContextTrait;
3548

3649
/**
3750
* @Flow\Inject
3851
* @var PersistenceManagerInterface
3952
*/
4053
protected $persistenceManager;
4154

55+
/**
56+
* @Flow\Inject
57+
* @var AssetService
58+
*/
59+
protected $assetService;
60+
61+
/**
62+
* @Flow\Inject
63+
* @var CachingHelper
64+
*/
65+
protected $cachingHelper;
66+
67+
/**
68+
* @Flow\Inject
69+
* @var ContentReleaseManager
70+
*/
71+
protected $contentReleaseManager;
72+
73+
/**
74+
* @Flow\InjectConfiguration("startIncrementalReleaseOnAssetChange")
75+
* @var bool
76+
*/
77+
protected $startIncrementalReleaseOnAssetChange;
78+
79+
/**
80+
* WHY:
81+
* The default implementation of ContentCacheFlusher::registerAssetChange() does not flush the cache of the parent
82+
* nodes until and including the parent document node of all nodes that use that asset.
83+
*
84+
* What we want to accomplish:
85+
* When an asset is updated (including "replaced"), we want to
86+
* 1. flush the cache of the updated asset (with and without workspace hash)
87+
* 2. flush the cache of all parent nodes until (including) the parent document node of all nodes that use that
88+
* asset (assetUsage)
89+
* 3. Commit cache flushes to prevent race condition with node enumeration during incremental rendering
90+
* 3. trigger incremental rendering
91+
*/
4292
/**
4393
* @Flow\Around("method(Neos\Neos\Fusion\Cache\ContentCacheFlusher->registerAssetChange())")
4494
*/
4595
public function registerAssetChange(JoinPointInterface $joinPoint)
4696
{
97+
/* @var AssetInterface $asset */
4798
$asset = $joinPoint->getMethodArgument('asset');
4899

49100
if (!$asset->isInUse()) {
50101
return;
51102
}
52103

53-
// HINT: do not flush node where the asset is in use (because we have dynamic tags for this, and we are not allowed to flush documents)
104+
/* @var ContentCacheFlusher $contentCacheFlusher */
105+
$contentCacheFlusher = $joinPoint->getProxy();
54106

107+
// 1. flush asset tag without workspace hash
55108
$assetIdentifier = $this->persistenceManager->getIdentifierByObject($asset);
56-
// @see RuntimeContentCache.addTag
57109

58-
$tagName = 'AssetDynamicTag_' . $assetIdentifier;
59-
60-
$contentCacheFlusher = $joinPoint->getProxy();
110+
$assetCacheTag = "AssetDynamicTag_" . $assetIdentifier;
61111

112+
// WHY: ContentCacheFlusher has no public api to flush tags directly
62113
$tagsToFlush = ObjectAccess::getProperty($contentCacheFlusher, 'tagsToFlush', true);
63-
$tagsToFlush[$tagName] = sprintf('which were tagged with "%s" because asset "%s" has changed.', $tagName, $assetIdentifier);
114+
$tagsToFlush[$assetCacheTag] = sprintf('which were tagged with "%s" because asset "%s" has changed.', $assetCacheTag, $assetIdentifier);
64115
ObjectAccess::setProperty($contentCacheFlusher, 'tagsToFlush', $tagsToFlush, true);
116+
117+
$usageReferences = $this->assetService->getUsageReferences($asset);
118+
119+
foreach ($usageReferences as $assetUsage) {
120+
// get node that uses the asset
121+
$context = $this->_contextFactory->create(
122+
[
123+
'workspaceName' => $assetUsage->getWorkspaceName(),
124+
'dimensions' => $assetUsage->getDimensionValues(),
125+
'invisibleContentShown' => true,
126+
'removedContentShown' => true]
127+
);
128+
129+
$node = $context->getNodeByIdentifier($assetUsage->getNodeIdentifier());
130+
131+
// We need this for cache tag generation
132+
$workspaceHash = $this->cachingHelper->renderWorkspaceTagForContextNode($context->getWorkspaceName());
133+
134+
// 1. flush asset with workspace hash
135+
$assetCacheTagWithWorkspace = "AssetDynamicTag_" . $workspaceHash . "_" . $assetIdentifier;
136+
137+
// WHY: ContentCacheFlusher has no public api to flush tags directly
138+
$tagsToFlush = ObjectAccess::getProperty($contentCacheFlusher, 'tagsToFlush', true);
139+
$tagsToFlush[$assetCacheTagWithWorkspace] = sprintf('which were tagged with "%s" because asset "%s" has changed.', $assetCacheTagWithWorkspace, $assetIdentifier);
140+
ObjectAccess::setProperty($contentCacheFlusher, 'tagsToFlush', $tagsToFlush, true);
141+
142+
// 2. flush all nodes on path to parent document node (a bit excessive, but for now it works)
143+
$currentNode = $node;
144+
while ($currentNode->getParent() !== null) {
145+
// flush node cache
146+
$contentCacheFlusher->registerNodeChange($node);
147+
148+
// if document node, stop
149+
if ($currentNode->getNodeType()->isOfType('Neos.Neos:Document')) {
150+
break;
151+
}
152+
153+
// go to parent node
154+
$currentNode = $currentNode->getParent();
155+
}
156+
}
157+
158+
// 3. commit cache flushes
159+
// We need force the commit here because we run into a race condition otherwise, where the incremental rendering
160+
// is starting node enumeration before the cache flushes are actually committed.
161+
$contentCacheFlusher->shutdownObject();
162+
163+
// 4. trigger incremental rendering
164+
if ($this->startIncrementalReleaseOnAssetChange) {
165+
$this->contentReleaseManager->startIncrementalContentRelease();
166+
}
65167
}
66168
}

Configuration/Settings.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
Flowpack:
22
DecoupledContentStore:
3+
# Automatically start an incremental release when an asset is changed/replaced
4+
startIncrementalReleaseOnAssetChange: true
5+
36
redisContentStores:
47
# the "Primary" content store is the one Neos writes to during building the content release.
58
primary:

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,15 @@ As a big improvement for stability (compared to v1), the rendering pipeline does
239239
it is a full or an incremental render. To trigger a full render, the content cache is flushed before
240240
the rendering is started.
241241

242+
### Options
243+
After changing an Asset (e.g. in the Media Module) an incremental rendering is triggered.
244+
You can opt out of this behavior by setting the following configuration:
245+
````yaml
246+
Flowpack:
247+
DecoupledContentStore:
248+
startIncrementalReleaseOnAssetChange: false
249+
````
250+
242251
### What happens if edits happen during a rendering?
243252

244253
If a change by an editor happens during a rendering, the content cache is flushed (by tag) as a result of

0 commit comments

Comments
 (0)