Skip to content

Commit e368710

Browse files
V15: Add custom serializer for hybrid cache (#17727)
* Add custom serializer * Add migration to rebuild cache * Rename migration namespace to 15.1 * Also clear media cache * Remove failed cache items * Refactor to only use keys for document cache repository --------- Co-authored-by: nikolajlauridsen <[email protected]>
1 parent 4c009ab commit e368710

File tree

11 files changed

+121
-100
lines changed

11 files changed

+121
-100
lines changed

src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,5 +104,6 @@ protected virtual void DefinePlan()
104104
To<V_15_0_0.ConvertBlockGridEditorProperties>("{9D3CE7D4-4884-41D4-98E8-302EB6CB0CF6}");
105105
To<V_15_0_0.ConvertRichTextEditorProperties>("{37875E80-5CDD-42FF-A21A-7D4E3E23E0ED}");
106106
To<V_15_0_0.ConvertLocalLinks>("{42E44F9E-7262-4269-922D-7310CB48E724}");
107+
To<V_15_1_0.RebuildCacheMigration>("{7B51B4DE-5574-4484-993E-05D12D9ED703}");
107108
}
108109
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using Umbraco.Cms.Core.PublishedCache;
2+
3+
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_1_0;
4+
5+
[Obsolete("Will be removed in V18")]
6+
public class RebuildCacheMigration : MigrationBase
7+
{
8+
private readonly IDocumentCacheService _documentCacheService;
9+
private readonly IMediaCacheService _mediaCacheService;
10+
11+
public RebuildCacheMigration(IMigrationContext context, IDocumentCacheService documentCacheService, IMediaCacheService mediaCacheService) : base(context)
12+
{
13+
_documentCacheService = documentCacheService;
14+
_mediaCacheService = mediaCacheService;
15+
}
16+
17+
protected override void Migrate()
18+
{
19+
_documentCacheService.ClearMemoryCacheAsync(CancellationToken.None).GetAwaiter().GetResult();
20+
_mediaCacheService.ClearMemoryCacheAsync(CancellationToken.None).GetAwaiter().GetResult();
21+
}
22+
23+
}

src/Umbraco.PublishedCache.HybridCache/ContentCacheNode.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace Umbraco.Cms.Infrastructure.HybridCache;
44

55
// This is for cache performance reasons, see https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid?view=aspnetcore-9.0#reuse-objects
66
[ImmutableObject(true)]
7-
internal sealed class ContentCacheNode
7+
public sealed class ContentCacheNode
88
{
99
public int Id { get; set; }
1010

src/Umbraco.PublishedCache.HybridCache/ContentData.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Umbraco.Cms.Infrastructure.HybridCache;
77
/// </summary>
88
// This is for cache performance reasons, see https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid?view=aspnetcore-9.0#reuse-objects
99
[ImmutableObject(true)]
10-
internal sealed class ContentData
10+
public sealed class ContentData
1111
{
1212
public ContentData(string? name, string? urlSegment, int versionId, DateTime versionDate, int writerId, int? templateId, bool published, Dictionary<string, PropertyData[]>? properties, IReadOnlyDictionary<string, CultureVariation>? cultureInfos)
1313
{

src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11

22
using Microsoft.Extensions.DependencyInjection;
33
using Microsoft.Extensions.Options;
4-
using Umbraco.Cms.Core.Cache;
54
using Umbraco.Cms.Core.Configuration.Models;
65
using Umbraco.Cms.Core.DependencyInjection;
76
using Umbraco.Cms.Core.Notifications;
@@ -35,7 +34,7 @@ public static IUmbracoBuilder AddUmbracoHybridCache(this IUmbracoBuilder builder
3534
// We'll be a bit friendlier and default this to a higher value, you quickly hit the 1MB limit with a few languages and especially blocks.
3635
// This can be overwritten later if needed.
3736
options.MaximumPayloadBytes = 1024 * 1024 * 100; // 100MB
38-
});
37+
}).AddSerializer<ContentCacheNode, HybridCacheSerializer>();
3938
#pragma warning restore EXTEXP0018
4039
builder.Services.AddSingleton<IDatabaseCacheRepository, DatabaseCacheRepository>();
4140
builder.Services.AddSingleton<IPublishedContentCache, DocumentCache>();

src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -189,30 +189,6 @@ AND cmsContentNu.nodeId IS NULL
189189
return count == 0;
190190
}
191191

192-
public async Task<ContentCacheNode?> GetContentSourceAsync(int id, bool preview = false)
193-
{
194-
Sql<ISqlContext>? sql = SqlContentSourcesSelect()
195-
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document))
196-
.Append(SqlWhereNodeId(SqlContext, id))
197-
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
198-
199-
ContentSourceDto? dto = await Database.FirstOrDefaultAsync<ContentSourceDto>(sql);
200-
201-
if (dto == null)
202-
{
203-
return null;
204-
}
205-
206-
if (preview is false && dto.PubDataRaw is null && dto.PubData is null)
207-
{
208-
return null;
209-
}
210-
211-
IContentCacheDataSerializer serializer =
212-
_contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document);
213-
return CreateContentNodeKit(dto, serializer, preview);
214-
}
215-
216192
public async Task<ContentCacheNode?> GetContentSourceAsync(Guid key, bool preview = false)
217193
{
218194
Sql<ISqlContext>? sql = SqlContentSourcesSelect()
@@ -292,25 +268,6 @@ public IEnumerable<ContentCacheNode> GetContentByContentTypeKey(IEnumerable<Guid
292268
public IEnumerable<Guid> GetDocumentKeysByContentTypeKeys(IEnumerable<Guid> keys, bool published = false)
293269
=> GetContentSourceByDocumentTypeKey(keys, Constants.ObjectTypes.Document).Where(x => x.Published == published).Select(x => x.Key);
294270

295-
public async Task<ContentCacheNode?> GetMediaSourceAsync(int id)
296-
{
297-
Sql<ISqlContext>? sql = SqlMediaSourcesSelect()
298-
.Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media))
299-
.Append(SqlWhereNodeId(SqlContext, id))
300-
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
301-
302-
ContentSourceDto? dto = await Database.FirstOrDefaultAsync<ContentSourceDto>(sql);
303-
304-
if (dto is null)
305-
{
306-
return null;
307-
}
308-
309-
IContentCacheDataSerializer serializer =
310-
_contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media);
311-
return CreateMediaNodeKit(dto, serializer);
312-
}
313-
314271
public async Task<ContentCacheNode?> GetMediaSourceAsync(Guid key)
315272
{
316273
Sql<ISqlContext>? sql = SqlMediaSourcesSelect()

src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,8 @@ internal interface IDatabaseCacheRepository
77
{
88
Task DeleteContentItemAsync(int id);
99

10-
Task<ContentCacheNode?> GetContentSourceAsync(int id, bool preview = false);
11-
1210
Task<ContentCacheNode?> GetContentSourceAsync(Guid key, bool preview = false);
1311

14-
Task<ContentCacheNode?> GetMediaSourceAsync(int id);
15-
1612
Task<ContentCacheNode?> GetMediaSourceAsync(Guid key);
1713

1814

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System.Buffers;
2+
using MessagePack;
3+
using MessagePack.Resolvers;
4+
using Microsoft.Extensions.Caching.Hybrid;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization;
8+
9+
internal class HybridCacheSerializer : IHybridCacheSerializer<ContentCacheNode>
10+
{
11+
private readonly ILogger<HybridCacheSerializer> _logger;
12+
private readonly MessagePackSerializerOptions _options;
13+
14+
public HybridCacheSerializer(ILogger<HybridCacheSerializer> logger)
15+
{
16+
_logger = logger;
17+
MessagePackSerializerOptions defaultOptions = ContractlessStandardResolver.Options;
18+
IFormatterResolver resolver = CompositeResolver.Create(defaultOptions.Resolver);
19+
20+
_options = defaultOptions
21+
.WithResolver(resolver)
22+
.WithCompression(MessagePackCompression.Lz4BlockArray)
23+
.WithSecurity(MessagePackSecurity.UntrustedData);
24+
}
25+
26+
public ContentCacheNode Deserialize(ReadOnlySequence<byte> source)
27+
{
28+
try
29+
{
30+
return MessagePackSerializer.Deserialize<ContentCacheNode>(source, _options);
31+
}
32+
catch (MessagePackSerializationException ex)
33+
{
34+
_logger.LogError(ex, "Error deserializing ContentCacheNode");
35+
return null!;
36+
}
37+
}
38+
39+
public void Serialize(ContentCacheNode value, IBufferWriter<byte> target) => target.Write(MessagePackSerializer.Serialize(value, _options));
40+
}

src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -74,23 +74,7 @@ public DocumentCacheService(
7474
{
7575
bool calculatedPreview = preview ?? GetPreview();
7676

77-
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync(
78-
GetCacheKey(key, calculatedPreview), // Unique key to the cache entry
79-
async cancel =>
80-
{
81-
using ICoreScope scope = _scopeProvider.CreateCoreScope();
82-
ContentCacheNode? contentCacheNode = await _databaseCacheRepository.GetContentSourceAsync(key, calculatedPreview);
83-
scope.Complete();
84-
return contentCacheNode;
85-
},
86-
GetEntryOptions(key));
87-
88-
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, calculatedPreview).CreateModel(_publishedModelFactory);
89-
}
90-
91-
private bool GetPreview()
92-
{
93-
return _previewService.IsInPreview();
77+
return await GetNodeAsync(key, calculatedPreview);
9478
}
9579

9680
public async Task<IPublishedContent?> GetByIdAsync(int id, bool? preview = null)
@@ -104,17 +88,37 @@ private bool GetPreview()
10488
bool calculatedPreview = preview ?? GetPreview();
10589
Guid key = keyAttempt.Result;
10690

91+
return await GetNodeAsync(key, calculatedPreview);
92+
}
93+
94+
private async Task<IPublishedContent?> GetNodeAsync(Guid key, bool preview)
95+
{
96+
var cacheKey = GetCacheKey(key, preview);
97+
10798
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync(
108-
GetCacheKey(keyAttempt.Result, calculatedPreview), // Unique key to the cache entry
109-
async cancel =>
99+
cacheKey,
100+
async cancel =>
101+
{
102+
using ICoreScope scope = _scopeProvider.CreateCoreScope();
103+
ContentCacheNode? contentCacheNode = await _databaseCacheRepository.GetContentSourceAsync(key, preview);
104+
scope.Complete();
105+
return contentCacheNode;
106+
},
107+
GetEntryOptions(key));
108+
109+
// We don't want to cache removed items, this may cause issues if the L2 serializer changes.
110+
if (contentCacheNode is null)
110111
{
111-
using ICoreScope scope = _scopeProvider.CreateCoreScope();
112-
ContentCacheNode? contentCacheNode = await _databaseCacheRepository.GetContentSourceAsync(id, calculatedPreview);
113-
scope.Complete();
114-
return contentCacheNode;
115-
}, GetEntryOptions(key));
112+
await _hybridCache.RemoveAsync(cacheKey);
113+
return null;
114+
}
115+
116+
return _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview).CreateModel(_publishedModelFactory);
117+
}
116118

117-
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, calculatedPreview).CreateModel(_publishedModelFactory);;
119+
private bool GetPreview()
120+
{
121+
return _previewService.IsInPreview();
118122
}
119123

120124
public IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType)

src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,7 @@ public MediaCacheService(
7676
return null;
7777
}
7878

79-
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync(
80-
$"{key}", // Unique key to the cache entry
81-
async cancel =>
82-
{
83-
using ICoreScope scope = _scopeProvider.CreateCoreScope();
84-
ContentCacheNode? mediaCacheNode = await _databaseCacheRepository.GetMediaSourceAsync(idAttempt.Result);
85-
scope.Complete();
86-
return mediaCacheNode;
87-
}, GetEntryOptions(key));
88-
89-
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedMedia(contentCacheNode).CreateModel(_publishedModelFactory);
79+
return await GetNodeAsync(key);
9080
}
9181

9282
public async Task<IPublishedContent?> GetByIdAsync(int id)
@@ -96,19 +86,33 @@ public MediaCacheService(
9686
{
9787
return null;
9888
}
89+
9990
Guid key = keyAttempt.Result;
10091

92+
return await GetNodeAsync(key);
93+
}
94+
95+
private async Task<IPublishedContent?> GetNodeAsync(Guid key)
96+
{
97+
var cacheKey = $"{key}";
10198
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync(
102-
$"{keyAttempt.Result}", // Unique key to the cache entry
99+
cacheKey, // Unique key to the cache entry
103100
async cancel =>
104101
{
105102
using ICoreScope scope = _scopeProvider.CreateCoreScope();
106-
ContentCacheNode? mediaCacheNode = await _databaseCacheRepository.GetMediaSourceAsync(id);
103+
ContentCacheNode? mediaCacheNode = await _databaseCacheRepository.GetMediaSourceAsync(key);
107104
scope.Complete();
108105
return mediaCacheNode;
109106
}, GetEntryOptions(key));
110107

111-
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedMedia(contentCacheNode).CreateModel(_publishedModelFactory);
108+
// We don't want to cache removed items, this may cause issues if the L2 serializer changes.
109+
if (contentCacheNode is null)
110+
{
111+
await _hybridCache.RemoveAsync(cacheKey);
112+
return null;
113+
}
114+
115+
return _publishedContentFactory.ToIPublishedMedia(contentCacheNode).CreateModel(_publishedModelFactory);
112116
}
113117

114118
public async Task<bool> HasContentByIdAsync(int id)

0 commit comments

Comments
 (0)