Skip to content

Commit

Permalink
Caching: migrate HybridCache api surface from asp.net into runtime (#…
Browse files Browse the repository at this point in the history
…103103)

* HybridCache migration from aspnet

* use cancellationToken instead of token

* reapply dotnet/aspnetcore#56719

* Update src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/HybridCache.cs

Co-authored-by: David Cantú <[email protected]>

* Update src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/HybridCache.cs

Co-authored-by: David Cantú <[email protected]>

* Update src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/HybridCache.cs

Co-authored-by: David Cantú <[email protected]>

* prefer throw null

* use IEnumerable<string> instead of IReadOnlyCollection<string>

* remove suppressions

* Update src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/IHybridCacheSerializer.cs

Co-authored-by: Stephen Toub <[email protected]>

* Update src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/HybridCacheEntryOptions.cs

Co-authored-by: Stephen Toub <[email protected]>

* Update src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/HybridCacheEntryOptions.cs

Co-authored-by: Stephen Toub <[email protected]>

* PR nits

---------

Co-authored-by: David Cantú <[email protected]>
Co-authored-by: Stephen Toub <[email protected]>
  • Loading branch information
3 people authored Jul 22, 2024
1 parent 4c21cb3 commit ea9d53e
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ public partial interface IDistributedCache
void Set(string key, byte[] value, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions options);
System.Threading.Tasks.Task SetAsync(string key, byte[] value, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken));
}
public interface IBufferDistributedCache : IDistributedCache
{
bool TryGet(string key, System.Buffers.IBufferWriter<byte> destination);
System.Threading.Tasks.ValueTask<bool> TryGetAsync(string key, System.Buffers.IBufferWriter<byte> destination, System.Threading.CancellationToken token = default);
void Set(string key, System.Buffers.ReadOnlySequence<byte> value, DistributedCacheEntryOptions options);
System.Threading.Tasks.ValueTask SetAsync(string key, System.Buffers.ReadOnlySequence<byte> value, DistributedCacheEntryOptions options, System.Threading.CancellationToken token = default);
}
}
namespace Microsoft.Extensions.Caching.Memory
{
Expand Down Expand Up @@ -156,3 +163,55 @@ public SystemClock() { }
public System.DateTimeOffset UtcNow { get { throw null; } }
}
}
namespace Microsoft.Extensions.Caching.Hybrid
{
public partial interface IHybridCacheSerializer<T>
{
T Deserialize(System.Buffers.ReadOnlySequence<byte> source);
void Serialize(T value, System.Buffers.IBufferWriter<byte> target);
}
public interface IHybridCacheSerializerFactory
{
bool TryCreateSerializer<T>([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out IHybridCacheSerializer<T>? serializer);
}
public sealed class HybridCacheEntryOptions
{
public System.TimeSpan? Expiration { get; init; }
public System.TimeSpan? LocalCacheExpiration { get; init; }
public HybridCacheEntryFlags? Flags { get; init; }
}
[System.Flags]
public enum HybridCacheEntryFlags
{
None = 0,
DisableLocalCacheRead = 1 << 0,
DisableLocalCacheWrite = 1 << 1,
DisableLocalCache = DisableLocalCacheRead | DisableLocalCacheWrite,
DisableDistributedCacheRead = 1 << 2,
DisableDistributedCacheWrite = 1 << 3,
DisableDistributedCache = DisableDistributedCacheRead | DisableDistributedCacheWrite,
DisableUnderlyingData = 1 << 4,
DisableCompression = 1 << 5,
}
public abstract class HybridCache
{
public abstract System.Threading.Tasks.ValueTask<T> GetOrCreateAsync<TState, T>(string key, TState state, System.Func<TState, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>> factory,
HybridCacheEntryOptions? options = null, System.Collections.Generic.IEnumerable<string>? tags = null, System.Threading.CancellationToken cancellationToken = default);

public System.Threading.Tasks.ValueTask<T> GetOrCreateAsync<T>(string key, System.Func<System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>> factory,
HybridCacheEntryOptions? options = null, System.Collections.Generic.IEnumerable<string>? tags = null, System.Threading.CancellationToken cancellationToken = default)
=> throw null;

public abstract System.Threading.Tasks.ValueTask SetAsync<T>(string key, T value, HybridCacheEntryOptions? options = null, System.Collections.Generic.IEnumerable<string>? tags = null, System.Threading.CancellationToken cancellationToken = default);

public abstract System.Threading.Tasks.ValueTask RemoveAsync(string key, System.Threading.CancellationToken cancellationToken = default);

public virtual System.Threading.Tasks.ValueTask RemoveAsync(System.Collections.Generic.IEnumerable<string> keys, System.Threading.CancellationToken cancellationToken = default)
=> throw null;

public virtual System.Threading.Tasks.ValueTask RemoveByTagAsync(System.Collections.Generic.IEnumerable<string> tags, System.Threading.CancellationToken cancellationToken = default)
=> throw null;
public abstract System.Threading.Tasks.ValueTask RemoveByTagAsync(string tag, System.Threading.CancellationToken cancellationToken = default);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
<Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\IsExternalInit.cs"
Link="Common\System\Runtime\CompilerServices\IsExternalInit.cs" />

<PackageReference Include="System.Threading.Tasks.Extensions" Version="$(SystemThreadingTasksExtensionsVersion)" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;

namespace Microsoft.Extensions.Caching.Hybrid;

/// <summary>
/// Provides multi-tier caching services building on <see cref="IDistributedCache"/> backends.
/// </summary>
public abstract class HybridCache
{
/// <summary>
/// Asynchronously gets the value associated with the key if it exists, or generates a new entry using the provided key and a value from the given factory if the key is not found.
/// </summary>
/// <typeparam name="TState">The type of additional state required by <paramref name="factory"/>.</typeparam>
/// <typeparam name="T">The type of the data being considered.</typeparam>
/// <param name="key">The key of the entry to look for or create.</param>
/// <param name="factory">Provides the underlying data service is the data is not available in the cache.</param>
/// <param name="state">The state required for <paramref name="factory"/>.</param>
/// <param name="options">Additional options for this cache entry.</param>
/// <param name="tags">The tags to associate with this cache item.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The data, either from cache or the underlying data service.</returns>
public abstract ValueTask<T> GetOrCreateAsync<TState, T>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> factory,
HybridCacheEntryOptions? options = null, IEnumerable<string>? tags = null, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously gets the value associated with the key if it exists, or generates a new entry using the provided key and a value from the given factory if the key is not found.
/// </summary>
/// <typeparam name="T">The type of the data being considered.</typeparam>
/// <param name="key">The key of the entry to look for or create.</param>
/// <param name="factory">Provides the underlying data service is the data is not available in the cache.</param>
/// <param name="options">Additional options for this cache entry.</param>
/// <param name="tags">The tags to associate with this cache item.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The data, either from cache or the underlying data service.</returns>
public ValueTask<T> GetOrCreateAsync<T>(string key, Func<CancellationToken, ValueTask<T>> factory,
HybridCacheEntryOptions? options = null, IEnumerable<string>? tags = null, CancellationToken cancellationToken = default)
=> GetOrCreateAsync(key, factory, WrappedCallbackCache<T>.Instance, options, tags, cancellationToken);

private static class WrappedCallbackCache<T> // per-T memoized helper that allows GetOrCreateAsync<T> and GetOrCreateAsync<TState, T> to share an implementation
{
// for the simple usage scenario (no TState), pack the original callback as the "state", and use a wrapper function that just unrolls and invokes from the state
public static readonly Func<Func<CancellationToken, ValueTask<T>>, CancellationToken, ValueTask<T>> Instance = static (callback, ct) => callback(ct);
}

/// <summary>
/// Asynchronously sets or overwrites the value associated with the key.
/// </summary>
/// <typeparam name="T">The type of the data being considered.</typeparam>
/// <param name="key">The key of the entry to create.</param>
/// <param name="value">The value to assign for this cache entry.</param>
/// <param name="options">Additional options for this cache entry.</param>
/// <param name="tags">The tags to associate with this cache entry.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
public abstract ValueTask SetAsync<T>(string key, T value, HybridCacheEntryOptions? options = null, IEnumerable<string>? tags = null, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously removes the value associated with the key if it exists.
/// </summary>
public abstract ValueTask RemoveAsync(string key, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously removes the value associated with the key if it exists.
/// </summary>
/// <remarks>Implementors should treat <c>null</c> as empty</remarks>
public virtual ValueTask RemoveAsync(IEnumerable<string> keys, CancellationToken cancellationToken = default)
{
return keys switch
{
// for consistency with GetOrCreate/Set: interpret null as "none"
null or ICollection<string> { Count: 0 } => default,
ICollection<string> { Count: 1 } => RemoveAsync(keys.First(), cancellationToken),
_ => ForEachAsync(this, keys, cancellationToken),
};

// default implementation is to call RemoveAsync for each key in turn
static async ValueTask ForEachAsync(HybridCache @this, IEnumerable<string> keys, CancellationToken cancellationToken)
{
foreach (var key in keys)
{
await @this.RemoveAsync(key, cancellationToken).ConfigureAwait(false);
}
}
}

/// <summary>
/// Asynchronously removes all values associated with the specified tags.
/// </summary>
/// <remarks>Implementors should treat <c>null</c> as empty</remarks>
public virtual ValueTask RemoveByTagAsync(IEnumerable<string> tags, CancellationToken cancellationToken = default)
{
return tags switch
{
// for consistency with GetOrCreate/Set: interpret null as "none"
null or ICollection<string> { Count: 0 } => default,
ICollection<string> { Count: 1 } => RemoveByTagAsync(tags.Single(), cancellationToken),
_ => ForEachAsync(this, tags, cancellationToken),
};

// default implementation is to call RemoveByTagAsync for each key in turn
static async ValueTask ForEachAsync(HybridCache @this, IEnumerable<string> keys, CancellationToken cancellationToken)
{
foreach (var key in keys)
{
await @this.RemoveByTagAsync(key, cancellationToken).ConfigureAwait(false);
}
}
}

/// <summary>
/// Asynchronously removes all values associated with the specified tag.
/// </summary>
public abstract ValueTask RemoveByTagAsync(string tag, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace Microsoft.Extensions.Caching.Hybrid;

/// <summary>
/// Additional flags that apply to a <see cref="HybridCache"/> operation.
/// </summary>
[Flags]
public enum HybridCacheEntryFlags
{
/// <summary>
/// No additional flags.
/// </summary>
None = 0,
/// <summary>
/// Disables reading from the local in-process cache.
/// </summary>
DisableLocalCacheRead = 1 << 0,
/// <summary>
/// Disables writing to the local in-process cache.
/// </summary>
DisableLocalCacheWrite = 1 << 1,
/// <summary>
/// Disables both reading from and writing to the local in-process cache.
/// </summary>
DisableLocalCache = DisableLocalCacheRead | DisableLocalCacheWrite,
/// <summary>
/// Disables reading from the secondary distributed cache.
/// </summary>
DisableDistributedCacheRead = 1 << 2,
/// <summary>
/// Disables writing to the secondary distributed cache.
/// </summary>
DisableDistributedCacheWrite = 1 << 3,
/// <summary>
/// Disables both reading from and writing to the secondary distributed cache.
/// </summary>
DisableDistributedCache = DisableDistributedCacheRead | DisableDistributedCacheWrite,
/// <summary>
/// Only fetches the value from cache; does not attempt to access the underlying data store.
/// </summary>
DisableUnderlyingData = 1 << 4,
/// <summary>
/// Disables compression for this payload.
/// </summary>
DisableCompression = 1 << 5,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.Extensions.Caching.Distributed;

namespace Microsoft.Extensions.Caching.Hybrid;

/// <summary>
/// Additional options (expiration, etc.) that apply to a <see cref="HybridCache"/> operation. When options
/// can be specified at multiple levels (for example, globally and per-call), the values are composed; the
/// most granular non-null value is used, with null values being inherited. If no value is specified at
/// any level, the implementation may choose a reasonable default.
/// </summary>
public sealed class HybridCacheEntryOptions
{
/// <summary>
/// Gets or set the overall cache duration of this entry, passed to the backend distributed cache.
/// </summary>
public TimeSpan? Expiration { get; init; }

/// <remarks>
/// When retrieving a cached value from an external cache store, this value will be used to calculate the local
/// cache expiration, not exceeding the remaining overall cache lifetime.
/// </remarks>
public TimeSpan? LocalCacheExpiration { get; init; }

/// <summary>
/// Gets or sets additional flags that apply to the requested operation.
/// </summary>
public HybridCacheEntryFlags? Flags { get; init; }

// memoize when possible
private DistributedCacheEntryOptions? _dc;
internal DistributedCacheEntryOptions? ToDistributedCacheEntryOptions()
=> Expiration is null ? null : (_dc ??= new() { AbsoluteExpirationRelativeToNow = Expiration });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;

namespace Microsoft.Extensions.Caching.Hybrid;

/// <summary>
/// Per-type serialization/deserialization support for <see cref="HybridCache"/>.
/// </summary>
/// <typeparam name="T">The type being serialized/deserialized.</typeparam>
public interface IHybridCacheSerializer<T>
{
/// <summary>
/// Deserialize a <typeparamref name="T"/> value from the provided <paramref name="source"/>.
/// </summary>
T Deserialize(ReadOnlySequence<byte> source);

/// <summary>
/// Serialize <paramref name="value"/> to the provided <paramref name="target"/>.
/// </summary>
void Serialize(T value, IBufferWriter<byte> target);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Microsoft.Extensions.Caching.Hybrid;

/// <summary>
/// Factory provider for per-type <see cref="IHybridCacheSerializer{T}"/> instances.
/// </summary>
public interface IHybridCacheSerializerFactory
{
/// <summary>
/// Request a serializer for the provided type, if possible.
/// </summary>
/// <typeparam name="T">The type being serialized/deserialized.</typeparam>
/// <param name="serializer">The serializer.</param>
/// <returns><c>true</c> if the factory supports this type, <c>false</c> otherwise.</returns>
bool TryCreateSerializer<T>([NotNullWhen(true)] out IHybridCacheSerializer<T>? serializer);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Extensions.Caching.Distributed; // intentional for parity with IDistributedCache

/// <summary>
/// Represents a distributed cache of serialized values, with support for low allocation data transfer.
/// </summary>
public interface IBufferDistributedCache : IDistributedCache
{
/// <summary>
/// Attempt to retrieve an existing cache item.
/// </summary>
/// <param name="key">The unique key for the cache item.</param>
/// <param name="destination">The target to write the cache contents on success.</param>
/// <returns><c>true</c> if the cache item is found, <c>false</c> otherwise.</returns>
/// <remarks>This is functionally similar to <see cref="IDistributedCache.Get(string)"/>, but avoids the array allocation.</remarks>
bool TryGet(string key, IBufferWriter<byte> destination);

/// <summary>
/// Asynchronously attempt to retrieve an existing cache entry.
/// </summary>
/// <param name="key">The unique key for the cache entry.</param>
/// <param name="destination">The target to write the cache contents on success.</param>
/// <param name="token">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns><c>true</c> if the cache entry is found, <c>false</c> otherwise.</returns>
/// <remarks>This is functionally similar to <see cref="IDistributedCache.GetAsync(string, CancellationToken)"/>, but avoids the array allocation.</remarks>
ValueTask<bool> TryGetAsync(string key, IBufferWriter<byte> destination, CancellationToken token = default);

/// <summary>
/// Sets or overwrites a cache item.
/// </summary>
/// <param name="key">The key of the entry to create.</param>
/// <param name="value">The value for this cache entry.</param>
/// <param name="options">The cache options for the entry.</param>
/// <remarks>This is functionally similar to <see cref="IDistributedCache.Set(string, byte[], DistributedCacheEntryOptions)"/>, but avoids the array allocation.</remarks>
void Set(string key, ReadOnlySequence<byte> value, DistributedCacheEntryOptions options);

/// <summary>
/// Asynchronously sets or overwrites a cache entry.
/// </summary>
/// <param name="key">The key of the entry to create.</param>
/// <param name="value">The value for this cache entry.</param>
/// <param name="options">The cache options for the value.</param>
/// <param name="token">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <remarks>This is functionally similar to <see cref="IDistributedCache.SetAsync(string, byte[], DistributedCacheEntryOptions, CancellationToken)"/>, but avoids the array allocation.</remarks>
ValueTask SetAsync(string key, ReadOnlySequence<byte> value, DistributedCacheEntryOptions options, CancellationToken token = default);
}
Loading

0 comments on commit ea9d53e

Please sign in to comment.