Hybrid Cache: Resolve start-up errors with mis-matched types (#20554)
* Be consistent in use of GetOrCreateAsync overload in exists and retrieval.
Ensure nullability of ContentCacheNode is consistent in exists and retrieval.
* Applied suggestion from code review.
* Move seeding to Umbraco application starting rather than started, ensuring an initial request is served.
* Tighten up hybrid cache exists check with locking around check and remove, and use of cancellation token.
(cherry picked from commit 81a8a0c191)
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
@@ -74,7 +73,7 @@ public static class UmbracoBuilderExtensions
|
||||
builder.AddNotificationAsyncHandler<ContentTypeDeletedNotification, CacheRefreshingNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<MediaTypeRefreshedNotification, CacheRefreshingNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<MediaTypeDeletedNotification, CacheRefreshingNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<UmbracoApplicationStartedNotification, SeedingNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<UmbracoApplicationStartingNotification, SeedingNotificationHandler>();
|
||||
builder.AddCacheSeeding();
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Extensions;
|
||||
@@ -7,19 +8,24 @@ namespace Umbraco.Cms.Infrastructure.HybridCache.Extensions;
|
||||
/// </summary>
|
||||
internal static class HybridCacheExtensions
|
||||
{
|
||||
// Per-key semaphores to ensure the GetOrCreateAsync + RemoveAsync sequence
|
||||
// executes atomically for a given cache key.
|
||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _keyLocks = new();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the cache contains an item with a matching key.
|
||||
/// </summary>
|
||||
/// <param name="cache">An instance of <see cref="Microsoft.Extensions.Caching.Hybrid.HybridCache"/></param>
|
||||
/// <param name="key">The name (key) of the item to search for in the cache.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>True if the item exists already. False if it doesn't.</returns>
|
||||
/// <remarks>
|
||||
/// Hat-tip: https://github.com/dotnet/aspnetcore/discussions/57191
|
||||
/// Will never add or alter the state of any items in the cache.
|
||||
/// </remarks>
|
||||
public static async Task<bool> ExistsAsync<T>(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key)
|
||||
public static async Task<bool> ExistsAsync<T>(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key, CancellationToken token)
|
||||
{
|
||||
(bool exists, _) = await TryGetValueAsync<T>(cache, key);
|
||||
(bool exists, _) = await TryGetValueAsync<T>(cache, key, token).ConfigureAwait(false);
|
||||
return exists;
|
||||
}
|
||||
|
||||
@@ -29,34 +35,55 @@ internal static class HybridCacheExtensions
|
||||
/// <typeparam name="T">The type of the value of the item in the cache.</typeparam>
|
||||
/// <param name="cache">An instance of <see cref="Microsoft.Extensions.Caching.Hybrid.HybridCache"/></param>
|
||||
/// <param name="key">The name (key) of the item to search for in the cache.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>A tuple of <see cref="bool"/> and the object (if found) retrieved from the cache.</returns>
|
||||
/// <remarks>
|
||||
/// Hat-tip: https://github.com/dotnet/aspnetcore/discussions/57191
|
||||
/// Will never add or alter the state of any items in the cache.
|
||||
/// </remarks>
|
||||
public static async Task<(bool Exists, T? Value)> TryGetValueAsync<T>(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key)
|
||||
public static async Task<(bool Exists, T? Value)> TryGetValueAsync<T>(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key, CancellationToken token)
|
||||
{
|
||||
var exists = true;
|
||||
|
||||
T? result = await cache.GetOrCreateAsync<object, T>(
|
||||
key,
|
||||
null!,
|
||||
(_, _) =>
|
||||
{
|
||||
exists = false;
|
||||
return new ValueTask<T>(default(T)!);
|
||||
},
|
||||
new HybridCacheEntryOptions(),
|
||||
null,
|
||||
CancellationToken.None);
|
||||
// Acquire a per-key semaphore so that GetOrCreateAsync and the possible RemoveAsync
|
||||
// complete without another thread retrieving/creating the same key in-between.
|
||||
SemaphoreSlim sem = _keyLocks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
|
||||
|
||||
// In checking for the existence of the item, if not found, we will have created a cache entry with a null value.
|
||||
// So remove it again.
|
||||
if (exists is false)
|
||||
await sem.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await cache.RemoveAsync(key);
|
||||
}
|
||||
T? result = await cache.GetOrCreateAsync<T?>(
|
||||
key,
|
||||
cancellationToken =>
|
||||
{
|
||||
exists = false;
|
||||
return default;
|
||||
},
|
||||
new HybridCacheEntryOptions(),
|
||||
null,
|
||||
token).ConfigureAwait(false);
|
||||
|
||||
return (exists, result);
|
||||
// In checking for the existence of the item, if not found, we will have created a cache entry with a null value.
|
||||
// So remove it again. Because we're holding the per-key lock there is no chance another thread
|
||||
// will observe the temporary entry between GetOrCreateAsync and RemoveAsync.
|
||||
if (exists is false)
|
||||
{
|
||||
await cache.RemoveAsync(key).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return (exists, result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
sem.Release();
|
||||
|
||||
// Only remove the semaphore mapping if it still points to the same instance we used.
|
||||
// This avoids removing another thread's semaphore or corrupting the map.
|
||||
if (_keyLocks.TryGetValue(key, out SemaphoreSlim? current) && ReferenceEquals(current, sem))
|
||||
{
|
||||
_keyLocks.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
@@ -9,7 +9,7 @@ using Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers;
|
||||
|
||||
internal sealed class SeedingNotificationHandler : INotificationAsyncHandler<UmbracoApplicationStartedNotification>
|
||||
internal sealed class SeedingNotificationHandler : INotificationAsyncHandler<UmbracoApplicationStartingNotification>
|
||||
{
|
||||
private readonly IDocumentCacheService _documentCacheService;
|
||||
private readonly IMediaCacheService _mediaCacheService;
|
||||
@@ -24,7 +24,7 @@ internal sealed class SeedingNotificationHandler : INotificationAsyncHandler<Umb
|
||||
_globalSettings = globalSettings.Value;
|
||||
}
|
||||
|
||||
public async Task HandleAsync(UmbracoApplicationStartedNotification notification,
|
||||
public async Task HandleAsync(UmbracoApplicationStartingNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
|
||||
var cacheKey = GetCacheKey(key, false);
|
||||
|
||||
var existsInCache = await _hybridCache.ExistsAsync<ContentCacheNode>(cacheKey);
|
||||
var existsInCache = await _hybridCache.ExistsAsync<ContentCacheNode?>(cacheKey, cancellationToken).ConfigureAwait(false);
|
||||
if (existsInCache is false)
|
||||
{
|
||||
uncachedKeys.Add(key);
|
||||
@@ -278,7 +278,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _hybridCache.ExistsAsync<ContentCacheNode>(GetCacheKey(keyAttempt.Result, preview));
|
||||
return await _hybridCache.ExistsAsync<ContentCacheNode?>(GetCacheKey(keyAttempt.Result, preview), CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task RefreshContentAsync(IContent content)
|
||||
|
||||
@@ -133,7 +133,7 @@ internal sealed class MediaCacheService : IMediaCacheService
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _hybridCache.ExistsAsync<ContentCacheNode>($"{keyAttempt.Result}");
|
||||
return await _hybridCache.ExistsAsync<ContentCacheNode?>($"{keyAttempt.Result}", CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task RefreshMediaAsync(IMedia media)
|
||||
@@ -170,7 +170,7 @@ internal sealed class MediaCacheService : IMediaCacheService
|
||||
|
||||
var cacheKey = GetCacheKey(key, false);
|
||||
|
||||
var existsInCache = await _hybridCache.ExistsAsync<ContentCacheNode>(cacheKey);
|
||||
var existsInCache = await _hybridCache.ExistsAsync<ContentCacheNode?>(cacheKey, CancellationToken.None);
|
||||
if (existsInCache is false)
|
||||
{
|
||||
uncachedKeys.Add(key);
|
||||
|
||||
Reference in New Issue
Block a user