V15: Only cache items if all ancestors are published (#18337)
* Introduce IsDocumentPublishedInAnyCulture Sometimes we don't care about culture * Check ancestor path when resolving cache items * Fix tests * Rebuild NavigationService * Only set node if it has a published ancestor path * Remove branch when unpublished * Add tests * Add seed test * Consider published ancestor path when seeding documents * Introduce MediaBreadthFirstKeyProviderTests This is needed since the logic of document and media is no longer the same * Remove unused services * Move assert page to helper * Add variant tests * Add tests * Filter keys in ContentTypeSeedKeyProvider * Fix tests * Add failing test showing refreshing issue * Don't blow up if we can't resolve the node from navigation cache Turns out that this can actually happen :D Should be fine to just return false * Refactor cache refresher check * Make NavigationQueryService service protected * Add comment on how to refactor breadth first key provider * Refactor if statement
This commit is contained in:
@@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Navigation;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Factories;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Serialization;
|
||||
@@ -24,8 +25,11 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
private readonly IEnumerable<IDocumentSeedKeyProvider> _seedKeyProviders;
|
||||
private readonly IPublishedModelFactory _publishedModelFactory;
|
||||
private readonly IPreviewService _previewService;
|
||||
private readonly IPublishStatusQueryService _publishStatusQueryService;
|
||||
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
|
||||
private readonly CacheSettings _cacheSettings;
|
||||
private HashSet<Guid>? _seedKeys;
|
||||
|
||||
private HashSet<Guid> SeedKeys
|
||||
{
|
||||
get
|
||||
@@ -56,7 +60,9 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
IEnumerable<IDocumentSeedKeyProvider> seedKeyProviders,
|
||||
IOptions<CacheSettings> cacheSettings,
|
||||
IPublishedModelFactory publishedModelFactory,
|
||||
IPreviewService previewService)
|
||||
IPreviewService previewService,
|
||||
IPublishStatusQueryService publishStatusQueryService,
|
||||
IDocumentNavigationQueryService documentNavigationQueryService)
|
||||
{
|
||||
_databaseCacheRepository = databaseCacheRepository;
|
||||
_idKeyMap = idKeyMap;
|
||||
@@ -67,6 +73,8 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
_seedKeyProviders = seedKeyProviders;
|
||||
_publishedModelFactory = publishedModelFactory;
|
||||
_previewService = previewService;
|
||||
_publishStatusQueryService = publishStatusQueryService;
|
||||
_documentNavigationQueryService = documentNavigationQueryService;
|
||||
_cacheSettings = cacheSettings.Value;
|
||||
}
|
||||
|
||||
@@ -101,6 +109,20 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
{
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope();
|
||||
ContentCacheNode? contentCacheNode = await _databaseCacheRepository.GetContentSourceAsync(key, preview);
|
||||
|
||||
// If we can resolve the content cache node, we still need to check if the ancestor path is published.
|
||||
// This does cost some performance, but it's necessary to ensure that the content is actually published.
|
||||
// When unpublishing a node, a payload with RefreshBranch is published, so we don't have to worry about this.
|
||||
// Similarly, when a branch is published, next time the content is requested, the parent will be published,
|
||||
// this works because we don't cache null values.
|
||||
if (preview is false && contentCacheNode is not null)
|
||||
{
|
||||
if (HasPublishedAncestorPath(contentCacheNode.Key) is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
return contentCacheNode;
|
||||
},
|
||||
@@ -116,6 +138,28 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
return _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview).CreateModel(_publishedModelFactory);
|
||||
}
|
||||
|
||||
private bool HasPublishedAncestorPath(Guid contentKey)
|
||||
{
|
||||
var success = _documentNavigationQueryService.TryGetAncestorsKeys(contentKey, out IEnumerable<Guid> keys);
|
||||
if (success is false)
|
||||
{
|
||||
// This might happen is certain cases, since 0notifications are not ordered, for instance, if you save and publish a content node in the same scope.
|
||||
// In this case we'll try and update the node in the cache even though it hasn't been updated in the document navigation cache yet.
|
||||
// It's okay to just return false here, since the node will be loaded later when it's actually requested.
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (Guid key in keys)
|
||||
{
|
||||
if (_publishStatusQueryService.IsDocumentPublishedInAnyCulture(key) is false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool GetPreview()
|
||||
{
|
||||
return _previewService.IsInPreview();
|
||||
@@ -169,7 +213,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
}
|
||||
|
||||
ContentCacheNode? publishedNode = await _databaseCacheRepository.GetContentSourceAsync(key, false);
|
||||
if (publishedNode is not null)
|
||||
if (publishedNode is not null && HasPublishedAncestorPath(publishedNode.Key))
|
||||
{
|
||||
await _hybridCache.SetAsync(GetCacheKey(publishedNode.Key, false), publishedNode, GetEntryOptions(publishedNode.Key));
|
||||
}
|
||||
@@ -195,7 +239,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
var cacheKey = GetCacheKey(key, false);
|
||||
|
||||
// We'll use GetOrCreateAsync because it may be in the second level cache, in which case we don't have to re-seed.
|
||||
ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync<ContentCacheNode?>(
|
||||
ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync<ContentCacheNode?>(
|
||||
cacheKey,
|
||||
async cancel =>
|
||||
{
|
||||
@@ -212,17 +256,20 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
|
||||
return cacheNode;
|
||||
},
|
||||
GetSeedEntryOptions(),
|
||||
cancellationToken: cancellationToken);
|
||||
GetSeedEntryOptions(),
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
// If the value is null, it's likely because
|
||||
if (cachedValue is null)
|
||||
if (cachedValue is null)
|
||||
{
|
||||
await _hybridCache.RemoveAsync(cacheKey);
|
||||
await _hybridCache.RemoveAsync(cacheKey, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Internal for test purposes.
|
||||
internal void ResetSeedKeys() => _seedKeys = null;
|
||||
|
||||
private HybridCacheEntryOptions GetSeedEntryOptions() => new()
|
||||
{
|
||||
Expiration = _cacheSettings.Entry.Document.SeedCacheDuration,
|
||||
|
||||
Reference in New Issue
Block a user