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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user