V16: Implement cache tags (#19101)

* Implement tags for content cache

* Implement tags for media cache

* Refactor to only use cache and media tags

* Remove from DI

* Cleanup

* Update Nuget packages

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Change description to be more precise

* Minor code tidy: indents, static methods where possible, made tags methods a little terser.

* Fixed according to review

---------

Co-authored-by: Elitsa <elm@umbraco.dk>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
Nikolaj Geisle
2025-04-23 10:08:08 +02:00
committed by GitHub
parent 4114342de2
commit d568e981ab
10 changed files with 84 additions and 176 deletions

View File

@@ -124,7 +124,8 @@ internal sealed class DocumentCacheService : IDocumentCacheService
scope.Complete();
return contentCacheNode;
},
GetEntryOptions(key, preview));
GetEntryOptions(key, preview),
GenerateTags(key));
// We don't want to cache removed items, this may cause issues if the L2 serializer changes.
if (contentCacheNode is null)
@@ -158,10 +159,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
return true;
}
private bool GetPreview()
{
return _previewService.IsInPreview();
}
private bool GetPreview() => _previewService.IsInPreview();
public IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType)
{
@@ -176,28 +174,10 @@ internal sealed class DocumentCacheService : IDocumentCacheService
public async Task ClearMemoryCacheAsync(CancellationToken cancellationToken)
{
// TODO: This should be done with tags, however this is not implemented yet, so for now we have to naively get all content keys and clear them all.
using ICoreScope scope = _scopeProvider.CreateCoreScope();
// We have to get ALL document keys in order to be able to remove them from the cache,
IEnumerable<Guid> documentKeys = await _databaseCacheRepository.GetContentKeysAsync(Constants.ObjectTypes.Document);
foreach (Guid documentKey in documentKeys)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
// We'll remove both the draft and published cache
await _hybridCache.RemoveAsync(GetCacheKey(documentKey, false), cancellationToken);
await _hybridCache.RemoveAsync(GetCacheKey(documentKey, true), cancellationToken);
}
await _hybridCache.RemoveByTagAsync(Constants.Cache.Tags.Content, cancellationToken);
// We have to run seeding again after the cache is cleared
await SeedAsync(cancellationToken);
scope.Complete();
}
public async Task RefreshMemoryCacheAsync(Guid key)
@@ -207,13 +187,13 @@ internal sealed class DocumentCacheService : IDocumentCacheService
ContentCacheNode? draftNode = await _databaseCacheRepository.GetContentSourceAsync(key, true);
if (draftNode is not null)
{
await _hybridCache.SetAsync(GetCacheKey(draftNode.Key, true), draftNode, GetEntryOptions(draftNode.Key, true));
await _hybridCache.SetAsync(GetCacheKey(draftNode.Key, true), draftNode, GetEntryOptions(draftNode.Key, true), GenerateTags(key));
}
ContentCacheNode? publishedNode = await _databaseCacheRepository.GetContentSourceAsync(key, false);
if (publishedNode is not null && HasPublishedAncestorPath(publishedNode.Key))
{
await _hybridCache.SetAsync(GetCacheKey(publishedNode.Key, false), publishedNode, GetEntryOptions(publishedNode.Key, false));
await _hybridCache.SetAsync(GetCacheKey(publishedNode.Key, false), publishedNode, GetEntryOptions(publishedNode.Key, false), GenerateTags(key));
}
scope.Complete();
@@ -229,35 +209,37 @@ internal sealed class DocumentCacheService : IDocumentCacheService
{
foreach (Guid key in SeedKeys)
{
if(cancellationToken.IsCancellationRequested)
if (cancellationToken.IsCancellationRequested)
{
break;
}
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?>(
cacheKey,
async cancel =>
// 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(
cacheKey,
async cancel =>
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
ContentCacheNode? cacheNode = await _databaseCacheRepository.GetContentSourceAsync(key);
scope.Complete();
// We don't want to seed drafts
if (cacheNode is null || cacheNode.IsDraft)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
return null;
}
ContentCacheNode? cacheNode = await _databaseCacheRepository.GetContentSourceAsync(key, false);
return cacheNode;
},
GetSeedEntryOptions(),
GenerateTags(key),
cancellationToken: cancellationToken);
scope.Complete();
// We don't want to seed drafts
if (cacheNode is null || cacheNode.IsDraft)
{
return null;
}
return cacheNode;
},
GetSeedEntryOptions(),
cancellationToken: cancellationToken);
// If the value is null, it's likely because
// If the value is null, it's likely because
if (cachedValue is null)
{
await _hybridCache.RemoveAsync(cacheKey, cancellationToken);
@@ -290,7 +272,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
public async Task<bool> HasContentByIdAsync(int id, bool preview = false)
{
Attempt<Guid> keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document);
Attempt<Guid> keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document);
if (keyAttempt.Success is false)
{
return false;
@@ -315,7 +297,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
// Always set draft node
// We have nodes seperate in the cache, cause 99% of the time, you are only using one
// and thus we won't get too much data when retrieving from the cache.
ContentCacheNode draftCacheNode = _cacheNodeFactory.ToContentCacheNode(content, true);
var draftCacheNode = _cacheNodeFactory.ToContentCacheNode(content, true);
await _databaseCacheRepository.RefreshContentAsync(draftCacheNode, content.PublishedState);
@@ -329,13 +311,17 @@ internal sealed class DocumentCacheService : IDocumentCacheService
{
await _hybridCache.RemoveAsync(GetCacheKey(publishedCacheNode.Key, false));
}
}
scope.Complete();
}
private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
private static string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}";
// Generates the cache tags for a given CacheNode
// We use the tags to be able to clear all cache entries that are related to a given content item.
// Tags for now are only content/media, but can be expanded with draft/published later.
private static HashSet<string> GenerateTags(Guid? key) => key is null ? [] : [Constants.Cache.Tags.Content];
public async Task DeleteItemAsync(IContentBase content)
{
@@ -368,6 +354,5 @@ internal sealed class DocumentCacheService : IDocumentCacheService
_hybridCache.RemoveAsync(GetCacheKey(content.Key, false)).GetAwaiter().GetResult();
}
}
}
}