-
Notifications
You must be signed in to change notification settings - Fork 10.4k
[Blazor] Support HybridCache backend for persistent component state #62299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Microsoft.Extensions.Caching.Hybrid; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Microsoft.AspNetCore.Components.Server.Circuits; | ||
|
||
/// <summary> | ||
/// Default configuration for <see cref="CircuitOptions.HybridPersistenceCache"/>. | ||
/// </summary> | ||
internal sealed class DefaultHybridCache : IPostConfigureOptions<CircuitOptions> | ||
{ | ||
private readonly HybridCache? _hybridCache; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of <see cref="DefaultHybridCache"/>. | ||
/// </summary> | ||
/// <param name="hybridCache">The <see cref="HybridCache"/> service, if available.</param> | ||
public DefaultHybridCache(HybridCache? hybridCache = null) | ||
{ | ||
_hybridCache = hybridCache; | ||
ilonatommy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/// <inheritdoc /> | ||
public void PostConfigure(string? name, CircuitOptions options) | ||
{ | ||
// Only set the HybridPersistenceCache if it hasn't been explicitly configured | ||
// and a HybridCache service is available | ||
if (options.HybridPersistenceCache is null && _hybridCache is not null) | ||
{ | ||
options.HybridPersistenceCache = _hybridCache; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Microsoft.Extensions.Caching.Hybrid; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Microsoft.AspNetCore.Components.Server.Circuits; | ||
|
||
// Implementation of ICircuitPersistenceProvider that uses HybridCache for distributed caching | ||
internal sealed partial class HybridCacheCircuitPersistenceProvider : ICircuitPersistenceProvider | ||
{ | ||
private static readonly Func<CancellationToken, ValueTask<PersistedCircuitState>> _failOnCreate = | ||
static ct => throw new InvalidOperationException(); | ||
|
||
private static readonly string[] _tags = ["Microsoft.AspNetCore.Components.Server.PersistedCircuitState"]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the primary use of tags is for remove-by-tags-async, but I don't see that used; is this forward thinking to when that might be needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is about making sure that we are "good citizens" of the shared cache and that people have a way of specifically cleaning these entries. Is this the pattern or should I do it in a different way? |
||
|
||
private readonly SemaphoreSlim _lock = new(1, 1); | ||
private readonly HybridCache _hybridCache; | ||
private readonly ILogger<ICircuitPersistenceProvider> _logger; | ||
private readonly HybridCacheEntryOptions _cacheWriteOptions; | ||
private readonly HybridCacheEntryOptions _cacheReadOptions; | ||
|
||
public HybridCacheCircuitPersistenceProvider( | ||
HybridCache hybridCache, | ||
ILogger<ICircuitPersistenceProvider> logger, | ||
IOptions<CircuitOptions> options) | ||
{ | ||
_hybridCache = hybridCache; | ||
_logger = logger; | ||
_cacheWriteOptions = new HybridCacheEntryOptions | ||
{ | ||
Expiration = options.Value.PersistedCircuitDistributedRetentionPeriod, | ||
LocalCacheExpiration = options.Value.PersistedCircuitInMemoryRetentionPeriod, | ||
}; | ||
_cacheReadOptions = new HybridCacheEntryOptions | ||
{ | ||
Flags = HybridCacheEntryFlags.DisableLocalCacheWrite | | ||
HybridCacheEntryFlags.DisableDistributedCacheWrite | | ||
HybridCacheEntryFlags.DisableUnderlyingData, | ||
}; | ||
} | ||
|
||
public async Task PersistCircuitAsync(CircuitId circuitId, PersistedCircuitState persistedCircuitState, CancellationToken cancellation = default) | ||
{ | ||
Log.CircuitPauseStarted(_logger, circuitId); | ||
|
||
try | ||
{ | ||
await _lock.WaitAsync(cancellation); | ||
await _hybridCache.SetAsync(circuitId.Secret, persistedCircuitState, _cacheWriteOptions, _tags, cancellation); | ||
} | ||
catch (Exception ex) | ||
{ | ||
Log.ExceptionPersistingCircuit(_logger, circuitId, ex); | ||
} | ||
finally | ||
{ | ||
_lock.Release(); | ||
} | ||
} | ||
|
||
public async Task<PersistedCircuitState> RestoreCircuitAsync(CircuitId circuitId, CancellationToken cancellation = default) | ||
{ | ||
Log.CircuitResumeStarted(_logger, circuitId); | ||
|
||
try | ||
{ | ||
await _lock.WaitAsync(cancellation); | ||
var state = await _hybridCache.GetOrCreateAsync( | ||
circuitId.Secret, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know a lot about "circuitid", but: is this unique enough vs other potential parallel HC uses? there may be some benefit in a key prefix, and if you do that, I strongly recommend using interpolated string literals, i.e. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. edit: and then I see that this is only used in the "we need to remove it" case, in which case: you will need the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, the circuit id is 32 bytes of entropy so no chance we hit that :) |
||
factory: _failOnCreate, | ||
options: _cacheReadOptions, | ||
_tags, | ||
cancellation); | ||
|
||
if (state == null) | ||
{ | ||
Log.FailedToFindCircuitState(_logger, circuitId); | ||
return null; | ||
} | ||
|
||
await _hybridCache.RemoveAsync(circuitId.Secret, cancellation); | ||
|
||
Log.CircuitStateFound(_logger, circuitId); | ||
return state; | ||
} | ||
catch (Exception ex) | ||
{ | ||
Log.ExceptionRestoringCircuit(_logger, circuitId, ex); | ||
return null; | ||
} | ||
finally | ||
{ | ||
_lock.Release(); | ||
} | ||
} | ||
|
||
private static partial class Log | ||
{ | ||
[LoggerMessage(201, LogLevel.Debug, "Circuit state evicted for circuit {CircuitId} due to {Reason}", EventName = "CircuitStateEvicted")] | ||
public static partial void CircuitStateEvicted(ILogger logger, CircuitId circuitId, string reason); | ||
|
||
[LoggerMessage(202, LogLevel.Debug, "Resuming circuit with ID {CircuitId}", EventName = "CircuitResumeStarted")] | ||
public static partial void CircuitResumeStarted(ILogger logger, CircuitId circuitId); | ||
|
||
[LoggerMessage(203, LogLevel.Debug, "Failed to find persisted circuit with ID {CircuitId}", EventName = "FailedToFindCircuitState")] | ||
public static partial void FailedToFindCircuitState(ILogger logger, CircuitId circuitId); | ||
|
||
[LoggerMessage(204, LogLevel.Debug, "Circuit state found for circuit {CircuitId}", EventName = "CircuitStateFound")] | ||
public static partial void CircuitStateFound(ILogger logger, CircuitId circuitId); | ||
|
||
[LoggerMessage(205, LogLevel.Error, "An exception occurred while disposing the token source.", EventName = "ExceptionDisposingTokenSource")] | ||
public static partial void ExceptionDisposingTokenSource(ILogger logger, Exception exception); | ||
|
||
[LoggerMessage(206, LogLevel.Debug, "Pausing circuit with ID {CircuitId}", EventName = "CircuitPauseStarted")] | ||
public static partial void CircuitPauseStarted(ILogger logger, CircuitId circuitId); | ||
|
||
[LoggerMessage(207, LogLevel.Error, "An exception occurred while persisting circuit {CircuitId}.", EventName = "ExceptionPersistingCircuit")] | ||
public static partial void ExceptionPersistingCircuit(ILogger logger, CircuitId circuitId, Exception exception); | ||
|
||
[LoggerMessage(208, LogLevel.Error, "An exception occurred while restoring circuit {CircuitId}.", EventName = "ExceptionRestoringCircuit")] | ||
public static partial void ExceptionRestoringCircuit(ILogger logger, CircuitId circuitId, Exception exception); | ||
|
||
[LoggerMessage(209, LogLevel.Error, "An exception occurred during expiration handling for circuit {CircuitId}.", EventName = "ExceptionDuringExpiration")] | ||
public static partial void ExceptionDuringExpiration(ILogger logger, CircuitId circuitId, Exception exception); | ||
|
||
[LoggerMessage(210, LogLevel.Error, "An exception occurred while removing expired circuit {CircuitId}.", EventName = "ExceptionRemovingExpiredCircuit")] | ||
public static partial void ExceptionRemovingExpiredCircuit(ILogger logger, CircuitId circuitId, Exception exception); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wtgodbe @mgravell Do you know why there aren't 10.0.0-preview6 versions on our feeds?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/cc @sebastienros