diff --git a/AspNetCore.sln b/AspNetCore.sln index aba0325d0351..5a54800dae83 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1788,12 +1788,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components.WasmRemoteAuthen EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "src\OpenApi\sample\Sample.csproj", "{6DEC24A8-A166-432F-8E3B-58FFCDA92F52}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hybrid", "Hybrid", "{2D64CA23-6E81-488E-A7D3-9BDF87240098}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Caching.Hybrid", "src\Caching\Hybrid\src\Microsoft.Extensions.Caching.Hybrid.csproj", "{2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Caching.Hybrid.Tests", "src\Caching\Hybrid\test\Microsoft.Extensions.Caching.Hybrid.Tests.csproj", "{CF63C942-895A-4F6B-888A-7653D7C4991A}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MicroBenchmarks", "MicroBenchmarks", "{6469F11E-8CEE-4292-820B-324DFFC88EBC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Caching.MicroBenchmarks", "src\Caching\perf\MicroBenchmarks\Microsoft.Extensions.Caching.MicroBenchmarks\Microsoft.Extensions.Caching.MicroBenchmarks.csproj", "{8D2CC6ED-5105-4F52-8757-C21F4DE78589}" @@ -10821,38 +10815,6 @@ Global {6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|x64.Build.0 = Release|Any CPU {6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|x86.ActiveCfg = Release|Any CPU {6DEC24A8-A166-432F-8E3B-58FFCDA92F52}.Release|x86.Build.0 = Release|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Debug|arm64.ActiveCfg = Debug|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Debug|arm64.Build.0 = Debug|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Debug|x64.ActiveCfg = Debug|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Debug|x64.Build.0 = Debug|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Debug|x86.ActiveCfg = Debug|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Debug|x86.Build.0 = Debug|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Release|Any CPU.Build.0 = Release|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Release|arm64.ActiveCfg = Release|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Release|arm64.Build.0 = Release|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Release|x64.ActiveCfg = Release|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Release|x64.Build.0 = Release|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Release|x86.ActiveCfg = Release|Any CPU - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9}.Release|x86.Build.0 = Release|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Debug|arm64.ActiveCfg = Debug|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Debug|arm64.Build.0 = Debug|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Debug|x64.ActiveCfg = Debug|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Debug|x64.Build.0 = Debug|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Debug|x86.ActiveCfg = Debug|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Debug|x86.Build.0 = Debug|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Release|Any CPU.Build.0 = Release|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Release|arm64.ActiveCfg = Release|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Release|arm64.Build.0 = Release|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Release|x64.ActiveCfg = Release|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Release|x64.Build.0 = Release|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Release|x86.ActiveCfg = Release|Any CPU - {CF63C942-895A-4F6B-888A-7653D7C4991A}.Release|x86.Build.0 = Release|Any CPU {8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|Any CPU.Build.0 = Debug|Any CPU {8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -11880,9 +11842,6 @@ Global {433F91E4-E39D-4EB0-B798-2998B3969A2C} = {6126DCE4-9692-4EE2-B240-C65743572995} {8A021D6D-7935-4AB3-BB47-38D4FF9B0D13} = {6126DCE4-9692-4EE2-B240-C65743572995} {6DEC24A8-A166-432F-8E3B-58FFCDA92F52} = {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B} - {2D64CA23-6E81-488E-A7D3-9BDF87240098} = {0F39820F-F4A5-41C6-9809-D79B68F032EF} - {2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9} = {2D64CA23-6E81-488E-A7D3-9BDF87240098} - {CF63C942-895A-4F6B-888A-7653D7C4991A} = {2D64CA23-6E81-488E-A7D3-9BDF87240098} {6469F11E-8CEE-4292-820B-324DFFC88EBC} = {0F39820F-F4A5-41C6-9809-D79B68F032EF} {8D2CC6ED-5105-4F52-8757-C21F4DE78589} = {6469F11E-8CEE-4292-820B-324DFFC88EBC} {9DC6B242-457B-4767-A84B-C3D23B76C642} = {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B} diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 43600612a50b..d95f9cd46411 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -5,7 +5,6 @@ --> - diff --git a/eng/ShippingAssemblies.props b/eng/ShippingAssemblies.props index 94197d5274ae..0f6c1a3cdd4e 100644 --- a/eng/ShippingAssemblies.props +++ b/eng/ShippingAssemblies.props @@ -105,7 +105,6 @@ - diff --git a/src/Caching/Caching.slnf b/src/Caching/Caching.slnf index 522505d8ab90..094d2826ef12 100644 --- a/src/Caching/Caching.slnf +++ b/src/Caching/Caching.slnf @@ -2,8 +2,6 @@ "solution": { "path": "..\\..\\AspNetCore.sln", "projects": [ - "src\\Caching\\Hybrid\\src\\Microsoft.Extensions.Caching.Hybrid.csproj", - "src\\Caching\\Hybrid\\test\\Microsoft.Extensions.Caching.Hybrid.Tests.csproj", "src\\Caching\\SqlServer\\src\\Microsoft.Extensions.Caching.SqlServer.csproj", "src\\Caching\\SqlServer\\test\\Microsoft.Extensions.Caching.SqlServer.Tests.csproj", "src\\Caching\\StackExchangeRedis\\src\\Microsoft.Extensions.Caching.StackExchangeRedis.csproj", diff --git a/src/Caching/Hybrid/src/HybridCacheBuilderExtensions.cs b/src/Caching/Hybrid/src/HybridCacheBuilderExtensions.cs deleted file mode 100644 index 5ab0768496ef..000000000000 --- a/src/Caching/Hybrid/src/HybridCacheBuilderExtensions.cs +++ /dev/null @@ -1,59 +0,0 @@ -// 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; -using Microsoft.Extensions.Caching.Hybrid; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Configuration extension methods for / . -/// -public static class HybridCacheBuilderExtensions -{ - /// - /// Serialize values of type with the specified serializer from . - /// - public static IHybridCacheBuilder AddSerializer(this IHybridCacheBuilder builder, IHybridCacheSerializer serializer) - { - builder.Services.AddSingleton>(serializer); - return builder; - } - - /// - /// Serialize values of type with the serializer of type . - /// - public static IHybridCacheBuilder AddSerializer(this IHybridCacheBuilder builder) - where TImplementation : class, IHybridCacheSerializer - { - builder.Services.AddSingleton, TImplementation>(); - return builder; - } - - /// - /// Add as an additional serializer factory, which can provide serializers for multiple types. - /// - public static IHybridCacheBuilder AddSerializerFactory(this IHybridCacheBuilder builder, IHybridCacheSerializerFactory factory) - { - builder.Services.AddSingleton(factory); - return builder; - } - - /// - /// Add a factory of type as an additional serializer factory, which can provide serializers for multiple types. - /// - public static IHybridCacheBuilder AddSerializerFactory< -#if NET5_0_OR_GREATER - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - TImplementation>(this IHybridCacheBuilder builder) - where TImplementation : class, IHybridCacheSerializerFactory - { - builder.Services.AddSingleton(); - return builder; - } -} diff --git a/src/Caching/Hybrid/src/HybridCacheOptions.cs b/src/Caching/Hybrid/src/HybridCacheOptions.cs deleted file mode 100644 index 65c0709764fc..000000000000 --- a/src/Caching/Hybrid/src/HybridCacheOptions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Caching.Hybrid; - -/// -/// Options for configuring the default implementation. -/// -public class HybridCacheOptions // : IOptions -{ - // TODO: should we implement IOptions? - - /// - /// Default global options to be applied to operations; if options are - /// specified at the individual call level, the non-null values are merged (with the per-call - /// options being used in preference to the global options). If no value is specified for a given - /// option (globally or per-call), the implementation may choose a reasonable default. - /// - public HybridCacheEntryOptions? DefaultEntryOptions { get; set; } - - /// - /// Disallow compression for this instance. - /// - public bool DisableCompression { get; set; } - - /// - /// The maximum size of cache items; attempts to store values over this size will be logged - /// and the value will not be stored in cache. - /// - /// The default value is 1 MiB. - public long MaximumPayloadBytes { get; set; } = 1 << 20; // 1MiB - - /// - /// The maximum permitted length (in characters) of keys; attempts to use keys over this size will be logged. - /// - /// The default value is 1024 characters. - public int MaximumKeyLength { get; set; } = 1024; // characters - - /// - /// Use "tags" data as dimensions on metric reporting; if enabled, care should be used to ensure that - /// tags do not contain data that should not be visible in metrics systems. - /// - public bool ReportTagMetrics { get; set; } - - // HybridCacheOptions IOptions.Value => this; -} diff --git a/src/Caching/Hybrid/src/HybridCacheServiceExtensions.cs b/src/Caching/Hybrid/src/HybridCacheServiceExtensions.cs deleted file mode 100644 index f8aedbfe95ee..000000000000 --- a/src/Caching/Hybrid/src/HybridCacheServiceExtensions.cs +++ /dev/null @@ -1,53 +0,0 @@ -// 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.Hybrid; -using Microsoft.Extensions.Caching.Hybrid.Internal; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Configuration extension methods for . -/// -public static class HybridCacheServiceExtensions -{ - /// - /// Adds support for multi-tier caching services. - /// - /// A builder instance that allows further configuration of the system. - public static IHybridCacheBuilder AddHybridCache(this IServiceCollection services, Action setupAction) - { -#if NET7_0_OR_GREATER - ArgumentNullException.ThrowIfNull(setupAction); -#else - _ = setupAction ?? throw new ArgumentNullException(nameof(setupAction)); -#endif - AddHybridCache(services); - services.Configure(setupAction); - return new HybridCacheBuilder(services); - } - - /// - /// Adds support for multi-tier caching services. - /// - /// A builder instance that allows further configuration of the system. - public static IHybridCacheBuilder AddHybridCache(this IServiceCollection services) - { -#if NET7_0_OR_GREATER - ArgumentNullException.ThrowIfNull(services); -#else - _ = services ?? throw new ArgumentNullException(nameof(services)); -#endif - - services.TryAddSingleton(TimeProvider.System); - services.AddOptions(); - services.AddMemoryCache(); - services.TryAddSingleton(); - services.TryAddSingleton>(InbuiltTypeSerializer.Instance); - services.TryAddSingleton>(InbuiltTypeSerializer.Instance); - services.TryAddSingleton(); - return new HybridCacheBuilder(services); - } -} diff --git a/src/Caching/Hybrid/src/IHybridCacheBuilder.cs b/src/Caching/Hybrid/src/IHybridCacheBuilder.cs deleted file mode 100644 index c46c8b650858..000000000000 --- a/src/Caching/Hybrid/src/IHybridCacheBuilder.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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.DependencyInjection; - -namespace Microsoft.Extensions.Caching.Hybrid; - -/// -/// Helper API for configuring . -/// -public interface IHybridCacheBuilder -{ - /// - /// Gets the services collection associated with this instance. - /// - IServiceCollection Services { get; } -} - -internal sealed class HybridCacheBuilder(IServiceCollection services) : IHybridCacheBuilder -{ - public IServiceCollection Services { get; } = services; -} diff --git a/src/Caching/Hybrid/src/Internal/BufferChunk.cs b/src/Caching/Hybrid/src/Internal/BufferChunk.cs deleted file mode 100644 index 73b89776c287..000000000000 --- a/src/Caching/Hybrid/src/Internal/BufferChunk.cs +++ /dev/null @@ -1,82 +0,0 @@ -// 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.Buffers; -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; - -// used to convey buffer status; like ArraySegment, but Offset is always -// zero, and we use the most significant bit (MSB, the sign flag) of the length -// to track whether or not to recycle this value -internal readonly struct BufferChunk -{ - private const int MSB = (1 << 31); - - private readonly int _lengthAndPoolFlag; - public byte[]? Array { get; } // null for default - - public int Length => _lengthAndPoolFlag & ~MSB; - - public bool ReturnToPool => (_lengthAndPoolFlag & MSB) != 0; - - public byte[] ToArray() - { - var length = Length; - if (length == 0) - { - return []; - } - - var copy = new byte[length]; - Buffer.BlockCopy(Array!, 0, copy, 0, length); - return copy; - } - - public BufferChunk(byte[] array) - { - Debug.Assert(array is not null, "expected valid array input"); - Array = array; - _lengthAndPoolFlag = array.Length; - // assume not pooled, if exact-sized - // (we don't expect array.Length to be negative; we're really just saying - // "we expect the result of assigning array.Length to _lengthAndPoolFlag - // to give the expected Length *and* not have the MSB set; we're just - // checking that we haven't fat-fingered our MSB logic) - Debug.Assert(!ReturnToPool, "do not return right-sized arrays"); - Debug.Assert(Length == array.Length, "array length not respected"); - } - - public BufferChunk(byte[] array, int length, bool returnToPool) - { - Debug.Assert(array is not null, "expected valid array input"); - Debug.Assert(length >= 0, "expected valid length"); - Array = array; - _lengthAndPoolFlag = length | (returnToPool ? MSB : 0); - Debug.Assert(ReturnToPool == returnToPool, "return-to-pool not respected"); - Debug.Assert(Length == length, "length not respected"); - } - - internal void RecycleIfAppropriate() - { - if (ReturnToPool) - { - ArrayPool.Shared.Return(Array!); - } - Unsafe.AsRef(in this) = default; // anti foot-shotgun double-return guard; not 100%, but worth doing - Debug.Assert(Array is null && !ReturnToPool, "expected clean slate after recycle"); - } - - internal ReadOnlySequence AsSequence() => Length == 0 ? default : new ReadOnlySequence(Array!, 0, Length); - - internal BufferChunk DoNotReturnToPool() - { - var copy = this; - Unsafe.AsRef(in copy._lengthAndPoolFlag) &= ~MSB; - Debug.Assert(copy.Length == Length, "same length expected"); - Debug.Assert(!copy.ReturnToPool, "do not return to pool"); - return copy; - } -} diff --git a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.CacheItem.cs b/src/Caching/Hybrid/src/Internal/DefaultHybridCache.CacheItem.cs deleted file mode 100644 index bb3ddee3ef11..000000000000 --- a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.CacheItem.cs +++ /dev/null @@ -1,99 +0,0 @@ -// 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.Diagnostics; -using System.Threading; -using Microsoft.Extensions.Caching.Memory; - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; - -partial class DefaultHybridCache -{ - internal abstract class CacheItem - { - private int _refCount = 1; // the number of pending operations against this cache item - // note: the ref count is the number of callers anticipating this value at any given time; initially, - // it is one for a simple "get the value" flow, but if another call joins with us, it'll be incremented; - // if either cancels, it will get decremented, with the entire flow being cancelled if it ever becomes - // zero - // this counter also drives cache lifetime, with the cache itself incrementing the count by one; in the - // case of mutable data, cache eviction may reduce this to zero (in cooperation with any concurrent readers, - // who incr/decr around their fetch), allowing safe buffer recycling - - internal int RefCount => Volatile.Read(ref _refCount); - - internal static readonly PostEvictionDelegate _sharedOnEviction = static (key, value, reason, state) => - { - if (value is CacheItem item) - { - item.Release(); - } - }; - - public virtual bool NeedsEvictionCallback => false; // do we need to call Release when evicted? - - protected virtual void OnFinalRelease() { } // any required release semantics - - public abstract bool TryReserveBuffer(out BufferChunk buffer); - - public abstract bool DebugIsImmutable { get; } - - public bool Release() // returns true ONLY for the final release step - { - var newCount = Interlocked.Decrement(ref _refCount); - Debug.Assert(newCount >= 0, "over-release detected"); - if (newCount == 0) - { - // perform per-item clean-up, i.e. buffer recycling (if defensive copies needed) - OnFinalRelease(); - return true; - } - return false; - } - - public bool TryReserve() - { - // this is basically interlocked increment, but with a check against: - // a) incrementing upwards from zero - // b) overflowing *back* to zero - var oldValue = Volatile.Read(ref _refCount); - do - { - if (oldValue is 0 or -1) - { - return false; // already burned, or about to roll around back to zero - } - - var updated = Interlocked.CompareExchange(ref _refCount, oldValue + 1, oldValue); - if (updated == oldValue) - { - return true; // we exchanged - } - oldValue = updated; // we failed, but we have an updated state - } while (true); - } - - } - - internal abstract class CacheItem : CacheItem - { - internal static CacheItem Create() => ImmutableTypeCache.IsImmutable ? new ImmutableCacheItem() : new MutableCacheItem(); - - // attempt to get a value that was *not* previously reserved - public abstract bool TryGetValue(out T value); - - // get a value that *was* reserved, countermanding our reservation in the process - public T GetReservedValue() - { - if (!TryGetValue(out var value)) - { - Throw(); - } - Release(); - return value; - - static void Throw() => throw new ObjectDisposedException("The cache item has been recycled before the value was obtained"); - } - } -} diff --git a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.Debug.cs b/src/Caching/Hybrid/src/Internal/DefaultHybridCache.Debug.cs deleted file mode 100644 index 9aaf3da4ed49..000000000000 --- a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.Debug.cs +++ /dev/null @@ -1,70 +0,0 @@ -// 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; -using System.Diagnostics.CodeAnalysis; -using System.Threading; - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; - -partial class DefaultHybridCache -{ - internal bool DebugTryGetCacheItem(string key, [NotNullWhen(true)] out CacheItem? value) - { - if (_localCache.TryGetValue(key, out var untyped) && untyped is CacheItem typed) - { - value = typed; - return true; - } - value = null; - return false; - } - -#if DEBUG // enable ref-counted buffers - - private int _outstandingBufferCount; - - internal int DebugOnlyGetOutstandingBuffers(bool flush = false) - => flush ? Interlocked.Exchange(ref _outstandingBufferCount, 0) : Volatile.Read(ref _outstandingBufferCount); - - [Conditional("DEBUG")] - internal void DebugOnlyDecrementOutstandingBuffers() - { - Interlocked.Decrement(ref _outstandingBufferCount); - } - - [Conditional("DEBUG")] - internal void DebugOnlyIncrementOutstandingBuffers() - { - Interlocked.Increment(ref _outstandingBufferCount); - } -#endif - - partial class MutableCacheItem - { - partial void DebugOnlyDecrementOutstandingBuffers(); - partial void DebugOnlyTrackBufferCore(DefaultHybridCache cache); - - [Conditional("DEBUG")] - internal void DebugOnlyTrackBuffer(DefaultHybridCache cache) => DebugOnlyTrackBufferCore(cache); - -#if DEBUG - private DefaultHybridCache? _cache; // for buffer-tracking - only enabled in DEBUG - partial void DebugOnlyDecrementOutstandingBuffers() - { - if (_buffer.ReturnToPool) - { - _cache?.DebugOnlyDecrementOutstandingBuffers(); - } - } - partial void DebugOnlyTrackBufferCore(DefaultHybridCache cache) - { - _cache = cache; - if (_buffer.ReturnToPool) - { - _cache?.DebugOnlyIncrementOutstandingBuffers(); - } - } -#endif - } -} diff --git a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.ImmutableCacheItem.cs b/src/Caching/Hybrid/src/Internal/DefaultHybridCache.ImmutableCacheItem.cs deleted file mode 100644 index dd245912f6ee..000000000000 --- a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.ImmutableCacheItem.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Threading; - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; - -partial class DefaultHybridCache -{ - private sealed class ImmutableCacheItem : CacheItem // used to hold types that do not require defensive copies - { - private T _value = default!; // deferred until SetValue - - public void SetValue(T value) => _value = value; - - private static ImmutableCacheItem? _sharedDefault; - - // get a shared instance that passes as "reserved"; doesn't need to be 100% singleton, - // but we don't want to break the reservation rules either; if we can't reserve: create new - public static ImmutableCacheItem GetReservedShared() - { - var obj = Volatile.Read(ref _sharedDefault); - if (obj is null || !obj.TryReserve()) - { - obj = new(); - obj.TryReserve(); // this is reliable on a new instance - Volatile.Write(ref _sharedDefault, obj); - } - return obj; - } - - public override bool TryGetValue(out T value) - { - value = _value; - return true; // always available - } - - public override bool TryReserveBuffer(out BufferChunk buffer) - { - buffer = default; - return false; // we don't have one to reserve! - } - - public override bool DebugIsImmutable => true; - } -} diff --git a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.L2.cs b/src/Caching/Hybrid/src/Internal/DefaultHybridCache.L2.cs deleted file mode 100644 index e0480abff5b8..000000000000 --- a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.L2.cs +++ /dev/null @@ -1,138 +0,0 @@ -// 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.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; - -partial class DefaultHybridCache -{ - internal ValueTask GetFromL2Async(string key, CancellationToken token) - { - switch (GetFeatures(CacheFeatures.BackendCache | CacheFeatures.BackendBuffers)) - { - case CacheFeatures.BackendCache: // legacy byte[]-based - var pendingLegacy = _backendCache!.GetAsync(key, token); -#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - if (!pendingLegacy.IsCompletedSuccessfully) -#else - if (pendingLegacy.Status != TaskStatus.RanToCompletion) -#endif - { - return new(AwaitedLegacy(pendingLegacy, this)); - } - return new(GetValidPayloadSegment(pendingLegacy.Result)); // already complete - case CacheFeatures.BackendCache | CacheFeatures.BackendBuffers: // IBufferWriter-based - var writer = RecyclableArrayBufferWriter.Create(MaximumPayloadBytes); - var cache = Unsafe.As(_backendCache!); // type-checked already - var pendingBuffers = cache.TryGetAsync(key, writer, token); - if (!pendingBuffers.IsCompletedSuccessfully) - { - return new(AwaitedBuffers(pendingBuffers, writer)); - } - BufferChunk result = pendingBuffers.GetAwaiter().GetResult() - ? new(writer.DetachCommitted(out var length), length, returnToPool: true) - : default; - writer.Dispose(); // it is not accidental that this isn't "using"; avoid recycling if not 100% sure what happened - return new(result); - } - return default; - - static async Task AwaitedLegacy(Task pending, DefaultHybridCache @this) - { - var bytes = await pending.ConfigureAwait(false); - return @this.GetValidPayloadSegment(bytes); - } - - static async Task AwaitedBuffers(ValueTask pending, RecyclableArrayBufferWriter writer) - { - BufferChunk result = await pending.ConfigureAwait(false) - ? new(writer.DetachCommitted(out var length), length, returnToPool: true) - : default; - writer.Dispose(); // it is not accidental that this isn't "using"; avoid recycling if not 100% sure what happened - return result; - } - } - - private BufferChunk GetValidPayloadSegment(byte[]? payload) - { - if (payload is not null) - { - if (payload.Length > MaximumPayloadBytes) - { - ThrowPayloadLengthExceeded(payload.Length); - } - return new(payload); - } - return default; - } - - [DoesNotReturn, MethodImpl(MethodImplOptions.NoInlining)] - private void ThrowPayloadLengthExceeded(int size) // splitting the exception bits out to a different method - { - // TODO: also log to logger (hence instance method) - throw new InvalidOperationException($"Maximum cache length ({MaximumPayloadBytes} bytes) exceeded"); - } - - internal ValueTask SetL2Async(string key, in BufferChunk buffer, HybridCacheEntryOptions? options, CancellationToken token) - { - Debug.Assert(buffer.Array is not null); - switch (GetFeatures(CacheFeatures.BackendCache | CacheFeatures.BackendBuffers)) - { - case CacheFeatures.BackendCache: // legacy byte[]-based - var arr = buffer.Array; - if (arr.Length != buffer.Length) - { - // we'll need a right-sized snapshot - arr = buffer.ToArray(); - } - return new(_backendCache!.SetAsync(key, arr, GetOptions(options), token)); - case CacheFeatures.BackendCache | CacheFeatures.BackendBuffers: // ReadOnlySequence-based - var cache = Unsafe.As(_backendCache!); // type-checked already - return cache.SetAsync(key, buffer.AsSequence(), GetOptions(options), token); - } - return default; - } - - private DistributedCacheEntryOptions GetOptions(HybridCacheEntryOptions? options) - { - DistributedCacheEntryOptions? result = null; - if (options is not null && options.Expiration.HasValue && options.Expiration.GetValueOrDefault() != _defaultExpiration) - { - result = ToDistributedCacheEntryOptions(options); - } - return result ?? _defaultDistributedCacheExpiration; - -#if NET8_0_OR_GREATER - // internal method memoizes this allocation; since it is "init", it is immutable (outside reflection) - [UnsafeAccessor(UnsafeAccessorKind.Method, Name = nameof(ToDistributedCacheEntryOptions))] - extern static DistributedCacheEntryOptions? ToDistributedCacheEntryOptions(HybridCacheEntryOptions options); -#else - // withoug that helper method, we'll just eat the alloc (down-level TFMs) - static DistributedCacheEntryOptions ToDistributedCacheEntryOptions(HybridCacheEntryOptions options) - => new() { AbsoluteExpirationRelativeToNow = options.Expiration }; -#endif - } - - internal void SetL1(string key, CacheItem value, HybridCacheEntryOptions? options) - { - if (value.TryReserve()) // incr ref-count for the the cache itself; this *may* be released via the NeedsEvictionCallback path - { - // based on CacheExtensions.Set, but with post-eviction recycling - using var cacheEntry = _localCache.CreateEntry(key); - cacheEntry.AbsoluteExpirationRelativeToNow = options?.LocalCacheExpiration ?? _defaultLocalCacheExpiration; - cacheEntry.Value = value; - if (value.NeedsEvictionCallback) - { - cacheEntry.RegisterPostEvictionCallback(CacheItem._sharedOnEviction); - } - } - } -} diff --git a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.MutableCacheItem.cs b/src/Caching/Hybrid/src/Internal/DefaultHybridCache.MutableCacheItem.cs deleted file mode 100644 index 892883d4a639..000000000000 --- a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.MutableCacheItem.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; - -partial class DefaultHybridCache -{ - private sealed partial class MutableCacheItem : CacheItem // used to hold types that require defensive copies - { - private IHybridCacheSerializer _serializer = null!; // deferred until SetValue - private BufferChunk _buffer; - - public void SetValue(ref BufferChunk buffer, IHybridCacheSerializer serializer) - { - _serializer = serializer; - _buffer = buffer; - buffer = default; // we're taking over the lifetime; the caller no longer has it! - } - - public void SetValue(T value, IHybridCacheSerializer serializer, int maxLength) - { - _serializer = serializer; - var writer = RecyclableArrayBufferWriter.Create(maxLength); - serializer.Serialize(value, writer); - - _buffer = new(writer.DetachCommitted(out var length), length, returnToPool: true); - writer.Dispose(); // no buffers left (we just detached them), but just in case of other logic - } - - public override bool NeedsEvictionCallback => _buffer.ReturnToPool; - - protected override void OnFinalRelease() - { - DebugOnlyDecrementOutstandingBuffers(); - _buffer.RecycleIfAppropriate(); - } - - public override bool TryGetValue(out T value) - { - if (!TryReserve()) // only if we haven't already burned - { - value = default!; - return false; - } - - try - { - value = _serializer.Deserialize(_buffer.AsSequence()); - return true; - } - finally - { - Release(); - } - } - - public override bool TryReserveBuffer(out BufferChunk buffer) - { - if (TryReserve()) // only if we haven't already burned - { - buffer = _buffer.DoNotReturnToPool(); // not up to them! - return true; - } - buffer = default; - return false; - } - - public override bool DebugIsImmutable => false; - } -} diff --git a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.Serialization.cs b/src/Caching/Hybrid/src/Internal/DefaultHybridCache.Serialization.cs deleted file mode 100644 index 0adf22b793be..000000000000 --- a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.Serialization.cs +++ /dev/null @@ -1,112 +0,0 @@ -// 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.Concurrent; -using System.ComponentModel; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Serialization; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; -partial class DefaultHybridCache -{ - // per instance cache of typed serializers; each serializer is a - // IHybridCacheSerializer for the corresponding Type, but we can't - // know which here - and undesirable to add an artificial non-generic - // IHybridCacheSerializer base that serves no other purpose - private readonly ConcurrentDictionary _serializers = new(); - - internal int MaximumPayloadBytes { get; } - - internal IHybridCacheSerializer GetSerializer() - { - return _serializers.TryGetValue(typeof(T), out var serializer) - ? Unsafe.As>(serializer) : ResolveAndAddSerializer(this); - - static IHybridCacheSerializer ResolveAndAddSerializer(DefaultHybridCache @this) - { - // it isn't critical that we get only one serializer instance during start-up; what matters - // is that we don't get a new serializer instance *every time* - var serializer = @this._services.GetService>(); - if (serializer is null) - { - foreach (var factory in @this._serializerFactories) - { - if (factory.TryCreateSerializer(out var current)) - { - serializer = current; - break; // we've already reversed the factories, so: the first hit is what we want - } - } - } - if (serializer is null) - { - throw new InvalidOperationException($"No {nameof(IHybridCacheSerializer)} configured for type '{typeof(T).Name}'"); - } - // store the result so we don't repeat this in future - @this._serializers[typeof(T)] = serializer; - return serializer; - } - } - - internal static class ImmutableTypeCache // lazy memoize; T doesn't change per cache instance - { - // note for blittable types: a pure struct will be a full copy every time - nothing shared to mutate - public static readonly bool IsImmutable = (typeof(T).IsValueType && IsBlittable()) || IsImmutable(typeof(T)); - } - - private static bool IsBlittable() // minimize the generic portion - { -#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - return !RuntimeHelpers.IsReferenceOrContainsReferences(); -#else - try // down-level: only blittable types can be pinned - { - // get a typed, zeroed, non-null boxed instance of the appropriate type - // (can't use (object)default(T), as that would box to null for nullable types) - var obj = FormatterServices.GetUninitializedObject(Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T)); - GCHandle.Alloc(obj, GCHandleType.Pinned).Free(); - return true; - } - catch - { - return false; - } -#endif - } - - private static bool IsImmutable(Type type) - { - // check for known types - if (type == typeof(string)) - { - return true; - } - - if (type.IsValueType) - { - // switch from Foo? to Foo if necessary - if (Nullable.GetUnderlyingType(type) is { } nullable) - { - type = nullable; - } - } - - if (type.IsValueType || (type.IsClass & type.IsSealed)) - { - // check for [ImmutableObject(true)]; note we're looking at this as a statement about - // the overall nullability; for example, a type could contain a private int[] field, - // where the field is mutable and the list is mutable; but if the type is annotated: - // we're trusting that the API and use-case is such that the type is immutable - return type.GetCustomAttribute() is { Immutable: true }; - } - // don't trust interfaces and non-sealed types; we might have any concrete - // type that has different behaviour - return false; - - } -} diff --git a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.Stampede.cs b/src/Caching/Hybrid/src/Internal/DefaultHybridCache.Stampede.cs deleted file mode 100644 index 009d95f0bb08..000000000000 --- a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.Stampede.cs +++ /dev/null @@ -1,107 +0,0 @@ -// 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.Concurrent; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; - -partial class DefaultHybridCache -{ - private readonly ConcurrentDictionary _currentOperations = new(); - - internal int DebugGetCallerCount(string key, HybridCacheEntryFlags? flags = null) - { - var stampedeKey = new StampedeKey(key, flags ?? _defaultFlags); - return _currentOperations.TryGetValue(stampedeKey, out var state) ? state.DebugCallerCount : 0; - } - - // returns true for a new session (in which case: we need to start the work), false for a pre-existing session - public bool GetOrCreateStampedeState(string key, HybridCacheEntryFlags flags, out StampedeState stampedeState, bool canBeCanceled) - { - var stampedeKey = new StampedeKey(key, flags); - - // double-checked locking to try to avoid unnecessary sessions in race conditions, - // while avoiding the lock completely whenever possible - if (TryJoinExistingSession(this, stampedeKey, out var existing)) - { - stampedeState = existing; - return false; // someone ELSE is running the work - } - - // most common scenario here, then, is that we're not fighting with anyone else; - // go ahead and create a placeholder state object and *try* to add it - stampedeState = new StampedeState(this, stampedeKey, canBeCanceled); - if (_currentOperations.TryAdd(stampedeKey, stampedeState)) - { - // successfully added; indeed, no-one else was fighting: we're done - return true; // the CURRENT caller is responsible for making the work happen - } - - // hmm; failed to add - there's concurrent activity on the same key; we're now - // in very rare race condition territory; go ahead and take a lock while we - // collect our thoughts - lock (GetPartitionedSyncLock(in stampedeKey)) // see notes in SyncLock.cs - { - // check again while we hold the lock - if (TryJoinExistingSession(this, stampedeKey, out existing)) - { - // we found an existing state we can join; do that - stampedeState.SetCanceled(); // to be thorough: mark our speculative one as doomed (no-one has seen it, though) - stampedeState = existing; // and replace with the one we found - return false; // someone ELSE is running the work - - // note that in this case we allocated a StampedeState that got dropped on - // the floor; in the grand scheme of things, that's OK; this is a rare outcome - } - - // and check whether the value was L1-cached by an outgoing operation (for *us* to check needs local-cache-read, - // and for *them* to have updated needs local-cache-write, but since the shared us/them key includes flags, - // we can skip this if *either* flag is set) - if ((flags & HybridCacheEntryFlags.DisableLocalCache) == 0 && _localCache.TryGetValue(key, out var untyped) - && untyped is CacheItem typed && typed.TryReserve()) - { - stampedeState.SetResultDirect(typed); - return false; // the work has ALREADY been done - } - - // otherwise, either nothing existed - or the thing that already exists can't be joined; - // in that case, go ahead and use the state that we invented a moment ago (outside of the lock) - _currentOperations[stampedeKey] = stampedeState; - return true; // the CURRENT caller is responsible for making the work happen - } - - static bool TryJoinExistingSession(DefaultHybridCache @this, in StampedeKey stampedeKey, - [NotNullWhen(true)] out StampedeState? stampedeState) - { - if (@this._currentOperations.TryGetValue(stampedeKey, out var found)) - { - if (found is not StampedeState tmp) - { - ThrowWrongType(stampedeKey.Key, found.Type, typeof(T)); - } - - if (tmp.TryAddCaller()) - { - // we joined an existing session - stampedeState = tmp; - return true; - } - } - stampedeState = null; - return false; - } - - [DoesNotReturn] - static void ThrowWrongType(string key, Type existingType, Type newType) - { - Debug.Assert(existingType != newType); - throw new InvalidOperationException($"All calls to {nameof(HybridCache)} with the same key should use the same data type; the same key is being used for '{existingType.FullName}' and '{newType.FullName}' data") - { - Data = { { "CacheKey", key } } - }; - } - } -} diff --git a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.StampedeKey.cs b/src/Caching/Hybrid/src/Internal/DefaultHybridCache.StampedeKey.cs deleted file mode 100644 index eeca9e589b13..000000000000 --- a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.StampedeKey.cs +++ /dev/null @@ -1,51 +0,0 @@ -// 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.Diagnostics.CodeAnalysis; - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; - -partial class DefaultHybridCache -{ - internal readonly struct StampedeKey : IEquatable - { - private readonly string _key; - private readonly HybridCacheEntryFlags _flags; - private readonly int _hashCode; // we know we'll need it; compute it once only - public StampedeKey(string key, HybridCacheEntryFlags flags) - { - // We'll use both the key *and* the flags as combined flag; in reality, we *expect* - // the flags to be consistent between calls on the same operation, and it must be - // noted that the *cache items* only use the key (not the flags), but: it gets - // very hard to grok what the correct behaviour should be if combining two calls - // with different flags, since they could have mutually exclusive behaviours! - - // As such, we'll treat conflicting calls entirely separately from a stampede - // perspective. - _key = key; - _flags = flags; -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - _hashCode = System.HashCode.Combine(key, flags); -#else - _hashCode = key.GetHashCode() ^ (int)flags; -#endif - } - - public string Key => _key; - public HybridCacheEntryFlags Flags => _flags; - - // allow direct access to the pre-computed hash-code, semantically emphasizing that - // this is a constant-time operation against a known value - internal int HashCode => _hashCode; - - public bool Equals(StampedeKey other) => _flags == other._flags & _key == other._key; - - public override bool Equals([NotNullWhen(true)] object? obj) - => obj is StampedeKey other && Equals(other); - - public override int GetHashCode() => _hashCode; - - public override string ToString() => $"{_key} ({_flags})"; - } -} diff --git a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.StampedeState.cs b/src/Caching/Hybrid/src/Internal/DefaultHybridCache.StampedeState.cs deleted file mode 100644 index c59378c3df0d..000000000000 --- a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.StampedeState.cs +++ /dev/null @@ -1,102 +0,0 @@ -// 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.Runtime.CompilerServices; -using System.Threading; - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; - -partial class DefaultHybridCache -{ - internal abstract class StampedeState -#if NETCOREAPP3_0_OR_GREATER - : IThreadPoolWorkItem -#endif - { - private readonly DefaultHybridCache _cache; - private readonly CacheItem _cacheItem; - - // because multiple callers can enlist, we need to track when the *last* caller cancels - // (and keep going until then); that means we need to run with custom cancellation - private readonly CancellationTokenSource? _sharedCancellation; - internal readonly CancellationToken SharedToken; // this might have a value even when _sharedCancellation is null - - // we expose the key as a by-ref readonly; this minimizes the stack work involved in passing the key around - // (both in terms of width and copy-semantics) - private readonly StampedeKey _key; - public ref readonly StampedeKey Key => ref _key; - protected CacheItem CacheItem => _cacheItem; - - /// - /// Create a stamped token optionally with shared cancellation support - /// - protected StampedeState(DefaultHybridCache cache, in StampedeKey key, CacheItem cacheItem, bool canBeCanceled) - { - _cache = cache; - _key = key; - _cacheItem = cacheItem; - if (canBeCanceled) - { - // if the first (or any) caller can't be cancelled; we'll never get to zero; no point tracking - // (in reality, all callers usually use the same path, so cancellation is usually "all" or "none") - _sharedCancellation = new(); - SharedToken = _sharedCancellation.Token; - } - else - { - SharedToken = CancellationToken.None; - } - } - - /// - /// Create a stamped token using a fixed cancellation token - /// - protected StampedeState(DefaultHybridCache cache, in StampedeKey key, CacheItem cacheItem, CancellationToken token) - { - _cache = cache; - _key = key; - _cacheItem = cacheItem; - SharedToken = token; - } - -#if !NETCOREAPP3_0_OR_GREATER - protected static readonly WaitCallback SharedWaitCallback = static obj => Unsafe.As(obj).Execute(); -#endif - - protected DefaultHybridCache Cache => _cache; - - public abstract void Execute(); - - protected int MaximumPayloadBytes => _cache.MaximumPayloadBytes; - - public override string ToString() => Key.ToString(); - - public abstract void SetCanceled(); - - public int DebugCallerCount => _cacheItem.RefCount; - - public abstract Type Type { get; } - - public void CancelCaller() - { - // note that TryAddCaller has protections to avoid getting back from zero - if (_cacheItem.Release()) - { - // we're the last to leave; turn off the lights - _sharedCancellation?.Cancel(); - SetCanceled(); - } - } - - public bool TryAddCaller() => _cacheItem.TryReserve(); - } - - private void RemoveStampedeState(in StampedeKey key) - { - lock (GetPartitionedSyncLock(in key)) // see notes in SyncLock.cs - { - _currentOperations.TryRemove(key, out _); - } - } -} diff --git a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.StampedeStateT.cs b/src/Caching/Hybrid/src/Internal/DefaultHybridCache.StampedeStateT.cs deleted file mode 100644 index b88ed5f236b7..000000000000 --- a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.StampedeStateT.cs +++ /dev/null @@ -1,287 +0,0 @@ -// 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.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; - -partial class DefaultHybridCache -{ - internal sealed class StampedeState : StampedeState - { - private readonly TaskCompletionSource>? _result; - private TState? _state; - private Func>? _underlying; // main data factory - private HybridCacheEntryOptions? _options; - private Task? _sharedUnwrap; // allows multiple non-cancellable callers to share a single task (when no defensive copy needed) - - public StampedeState(DefaultHybridCache cache, in StampedeKey key, bool canBeCanceled) - : base(cache, key, CacheItem.Create(), canBeCanceled) - { - _result = new(TaskCreationOptions.RunContinuationsAsynchronously); - } - - public override Type Type => typeof(T); - - public StampedeState(DefaultHybridCache cache, in StampedeKey key, CancellationToken token) - : base(cache, key, CacheItem.Create(), token) { } // no TCS in this case - this is for SetValue only - - public void QueueUserWorkItem(in TState state, Func> underlying, HybridCacheEntryOptions? options) - { - Debug.Assert(_underlying is null); - Debug.Assert(underlying is not null); - - // initialize the callback state - _state = state; - _underlying = underlying; - _options = options; - -#if NETCOREAPP3_0_OR_GREATER - ThreadPool.UnsafeQueueUserWorkItem(this, false); -#else - ThreadPool.UnsafeQueueUserWorkItem(SharedWaitCallback, this); -#endif - } - - public Task ExecuteDirectAsync(in TState state, Func> underlying, HybridCacheEntryOptions? options) - { - Debug.Assert(_underlying is null); - Debug.Assert(underlying is not null); - - // initialize the callback state - _state = state; - _underlying = underlying; - _options = options; - - return BackgroundFetchAsync(); - } - - public override void Execute() => _ = BackgroundFetchAsync(); - - private async Task BackgroundFetchAsync() - { - try - { - // read from L2 if appropriate - if ((Key.Flags & HybridCacheEntryFlags.DisableDistributedCacheRead) == 0) - { - var result = await Cache.GetFromL2Async(Key.Key, SharedToken).ConfigureAwait(false); - - if (result.Array is not null) - { - SetResultAndRecycleIfAppropriate(ref result); - return; - } - } - - // nothing from L2; invoke the underlying data store - if ((Key.Flags & HybridCacheEntryFlags.DisableUnderlyingData) == 0) - { - var cacheItem = SetResult(await _underlying!(_state!, SharedToken).ConfigureAwait(false)); - - // note that at this point we've already released most or all of the waiting callers; everything - // else here is background - - // write to L2 if appropriate - if ((Key.Flags & HybridCacheEntryFlags.DisableDistributedCacheWrite) == 0) - { - if (cacheItem.TryReserveBuffer(out var buffer)) - { - // mutable: we've already serialized it for the shared cache item - await Cache.SetL2Async(Key.Key, in buffer, _options, SharedToken).ConfigureAwait(false); - cacheItem.Release(); // because we reserved - } - else if (cacheItem.TryGetValue(out var value)) - { - // immutable: we'll need to do the serialize ourselves - var writer = RecyclableArrayBufferWriter.Create(MaximumPayloadBytes); // note this lifetime spans the SetL2Async - Cache.GetSerializer().Serialize(value, writer); - buffer = new(writer.GetBuffer(out var length), length, returnToPool: false); // writer still owns the buffer - await Cache.SetL2Async(Key.Key, in buffer, _options, SharedToken).ConfigureAwait(false); - writer.Dispose(); // recycle on success - } - } - } - else - { - // can't read from data store; implies we shouldn't write - // back to anywhere else, either - SetDefaultResult(); - } - } - catch (Exception ex) - { - SetException(ex); - } - } - - public Task> Task - { - get - { - Debug.Assert(_result is not null); - return _result is null ? Invalid() : _result.Task; - - static Task> Invalid() => System.Threading.Tasks.Task.FromException>(new InvalidOperationException("Task should not be accessed for non-shared instances")); - } - } - - private void SetException(Exception ex) - { - if (_result is not null) - { - Cache.RemoveStampedeState(in Key); - _result.TrySetException(ex); - } - } - - // ONLY set the result, without any other side-effects - internal void SetResultDirect(CacheItem value) - => _result?.TrySetResult(value); - - private void SetResult(CacheItem value) - { - if ((Key.Flags & HybridCacheEntryFlags.DisableLocalCacheWrite) == 0) - { - Cache.SetL1(Key.Key, value, _options); // we can do this without a TCS, for SetValue - } - - if (_result is not null) - { - Cache.RemoveStampedeState(in Key); - _result.TrySetResult(value); - } - } - - private void SetDefaultResult() - { - // note we don't store this dummy result in L1 or L2 - if (_result is not null) - { - Cache.RemoveStampedeState(in Key); - _result.TrySetResult(ImmutableCacheItem.GetReservedShared()); - } - } - - private void SetResultAndRecycleIfAppropriate(ref BufferChunk value) - { - // set a result from L2 cache - Debug.Assert(value.Array is not null, "expected buffer"); - - var serializer = Cache.GetSerializer(); - CacheItem cacheItem; - switch (CacheItem) - { - case ImmutableCacheItem immutable: - // deserialize; and store object; buffer can be recycled now - immutable.SetValue(serializer.Deserialize(new(value.Array!, 0, value.Length))); - value.RecycleIfAppropriate(); - cacheItem = immutable; - break; - case MutableCacheItem mutable: - // use the buffer directly as the backing in the cache-item; do *not* recycle now - mutable.SetValue(ref value, serializer); - mutable.DebugOnlyTrackBuffer(Cache); - cacheItem = mutable; - break; - default: - cacheItem = ThrowUnexpectedCacheItem(); - break; - } - SetResult(cacheItem); - } - - [DoesNotReturn] - private static CacheItem ThrowUnexpectedCacheItem() => throw new InvalidOperationException("Unexpected cache item"); - - private CacheItem SetResult(T value) - { - // set a result from a value we calculated directly - CacheItem cacheItem; - switch (CacheItem) - { - case ImmutableCacheItem immutable: - // no serialize needed - immutable.SetValue(value); - cacheItem = immutable; - break; - case MutableCacheItem mutable: - // serialization happens here - mutable.SetValue(value, Cache.GetSerializer(), MaximumPayloadBytes); - mutable.DebugOnlyTrackBuffer(Cache); - cacheItem = mutable; - break; - default: - cacheItem = ThrowUnexpectedCacheItem(); - break; - } - SetResult(cacheItem); - return cacheItem; - } - - public override void SetCanceled() => _result?.TrySetCanceled(SharedToken); - - internal ValueTask UnwrapReservedAsync() - { - var task = Task; -#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - if (task.IsCompletedSuccessfully) -#else - if (task.Status == TaskStatus.RanToCompletion) -#endif - { - return new(task.Result.GetReservedValue()); - } - - // if the type is immutable, callers can share the final step too (this may leave dangling - // reservation counters, but that's OK) - var result = ImmutableTypeCache.IsImmutable ? (_sharedUnwrap ??= Awaited(Task)) : Awaited(Task); - return new(result); - - static async Task Awaited(Task> task) - => (await task.ConfigureAwait(false)).GetReservedValue(); - } - - public ValueTask JoinAsync(CancellationToken token) - { - // if the underlying has already completed, and/or our local token can't cancel: we - // can simply wrap the shared task; otherwise, we need our own cancellation state - return token.CanBeCanceled && !Task.IsCompleted ? WithCancellation(this, token) : UnwrapReservedAsync(); - - static async ValueTask WithCancellation(StampedeState stampede, CancellationToken token) - { - var cancelStub = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - using var reg = token.Register(static obj => - { - ((TaskCompletionSource)obj!).TrySetResult(true); - }, cancelStub); - - CacheItem result; - try - { - var first = await System.Threading.Tasks.Task.WhenAny(stampede.Task, cancelStub.Task).ConfigureAwait(false); - if (ReferenceEquals(first, cancelStub.Task)) - { - // we expect this to throw, because otherwise we wouldn't have gotten here - token.ThrowIfCancellationRequested(); // get an appropriate exception - } - Debug.Assert(ReferenceEquals(first, stampede.Task)); - - // this has already completed, but we'll get the stack nicely - result = await stampede.Task.ConfigureAwait(false); - } - catch - { - stampede.CancelCaller(); - throw; - } - // outside the catch, so we know we only decrement one way or the other - return result.GetReservedValue(); - } - } - } -} diff --git a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.SyncLock.cs b/src/Caching/Hybrid/src/Internal/DefaultHybridCache.SyncLock.cs deleted file mode 100644 index c702f7e9b3f9..000000000000 --- a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.SyncLock.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; - -partial class DefaultHybridCache -{ - // HybridCache's stampede protection requires some level of synchronization to avoid unnecessary runs - // of the underlying data fetch; this is *minimized* by the use of double-checked locking and - // interlocked join (adding a new request to an existing execution), but: that would leave a race - // condition where the *remove* step of the stampede would be in a race with the *add new* step; the - // *add new* step is inside a lock, but we need to *remove* step to share that lock, to avoid - // the race. We deal with that by taking the same lock during remove, but *that* means we're locking - // on all executions. - // - // To minimize lock contention, we will therefore use partitioning of the lock-token, by using the - // low 3 bits of the hash-code (which we calculate eagerly only once, so: already known). This gives - // us a fast way to split contention by 8, almost an order-of-magnitude, which is sufficient. We *could* - // use an array for this, but: for directness, let's inline it instead (avoiding bounds-checks, - // an extra layer of dereferencing, and the allocation; I will acknowledge these are miniscule, but: - // it costs us nothing to do) - - private readonly object _syncLock0 = new(), _syncLock1 = new(), _syncLock2 = new(), _syncLock3 = new(), - _syncLock4 = new(), _syncLock5 = new(), _syncLock6 = new(), _syncLock7 = new(); - - internal object GetPartitionedSyncLock(in StampedeKey key) - => (key.HashCode & 0b111) switch // generate 8 partitions using the low 3 bits - { - 0 => _syncLock0, 1 => _syncLock1, - 2 => _syncLock2, 3 => _syncLock3, - 4 => _syncLock4, 5 => _syncLock5, - 6 => _syncLock6, _ => _syncLock7, - }; -} diff --git a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.cs b/src/Caching/Hybrid/src/Internal/DefaultHybridCache.cs deleted file mode 100644 index 3dc98e55ef3b..000000000000 --- a/src/Caching/Hybrid/src/Internal/DefaultHybridCache.cs +++ /dev/null @@ -1,164 +0,0 @@ -// 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.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; - -/// -/// The inbuilt ASP.NET implementation of . -/// -internal sealed partial class DefaultHybridCache : HybridCache -{ - private readonly IDistributedCache? _backendCache; - private readonly IMemoryCache _localCache; - private readonly IServiceProvider _services; // we can't resolve per-type serializers until we see each T - private readonly IHybridCacheSerializerFactory[] _serializerFactories; - private readonly HybridCacheOptions _options; - private readonly ILogger _logger; - private readonly CacheFeatures _features; // used to avoid constant type-testing - - private readonly HybridCacheEntryFlags _hardFlags; // *always* present (for example, because no L2) - private readonly HybridCacheEntryFlags _defaultFlags; // note this already includes hardFlags - private readonly TimeSpan _defaultExpiration; - private readonly TimeSpan _defaultLocalCacheExpiration; - - private readonly DistributedCacheEntryOptions _defaultDistributedCacheExpiration; - - [Flags] - internal enum CacheFeatures - { - None = 0, - BackendCache = 1 << 0, - BackendBuffers = 1 << 1, - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private CacheFeatures GetFeatures(CacheFeatures mask) => _features & mask; - - internal CacheFeatures GetFeatures() => _features; - - // used to restrict features in test suite - internal void DebugRemoveFeatures(CacheFeatures features) => Unsafe.AsRef(in _features) &= ~features; - - public DefaultHybridCache(IOptions options, IServiceProvider services) - { - _services = services ?? throw new ArgumentNullException(nameof(services)); - _localCache = services.GetRequiredService(); - _options = options.Value; - _logger = services.GetService()?.CreateLogger(typeof(HybridCache)) ?? NullLogger.Instance; - - _backendCache = services.GetService(); // note optional - - // ignore L2 if it is really just the same L1, wrapped - // (note not just an "is" test; if someone has a custom subclass, who knows what it does?) - if (_backendCache is not null - && _backendCache.GetType() == typeof(MemoryDistributedCache) - && _localCache.GetType() == typeof(MemoryCache)) - { - _backendCache = null; - } - - // perform type-tests on the backend once only - _features |= _backendCache switch - { - IBufferDistributedCache => CacheFeatures.BackendCache | CacheFeatures.BackendBuffers, - not null => CacheFeatures.BackendCache, - _ => CacheFeatures.None - }; - - // When resolving serializers via the factory API, we will want the *last* instance, - // i.e. "last added wins"; we can optimize by reversing the array ahead of time, and - // taking the first match - var factories = services.GetServices().ToArray(); - Array.Reverse(factories); - _serializerFactories = factories; - - MaximumPayloadBytes = checked((int)_options.MaximumPayloadBytes); // for now hard-limit to 2GiB - - var defaultEntryOptions = _options.DefaultEntryOptions; - - if (_backendCache is null) - { - _hardFlags |= HybridCacheEntryFlags.DisableDistributedCache; - } - _defaultFlags = (defaultEntryOptions?.Flags ?? HybridCacheEntryFlags.None) | _hardFlags; - _defaultExpiration = defaultEntryOptions?.Expiration ?? TimeSpan.FromMinutes(5); - _defaultLocalCacheExpiration = defaultEntryOptions?.LocalCacheExpiration ?? TimeSpan.FromMinutes(1); - _defaultDistributedCacheExpiration = new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = _defaultExpiration }; - } - - internal IDistributedCache? BackendCache => _backendCache; - internal IMemoryCache LocalCache => _localCache; - - internal HybridCacheOptions Options => _options; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private HybridCacheEntryFlags GetEffectiveFlags(HybridCacheEntryOptions? options) - => (options?.Flags | _hardFlags) ?? _defaultFlags; - - public override ValueTask GetOrCreateAsync(string key, TState state, Func> underlyingDataCallback, HybridCacheEntryOptions? options = null, IEnumerable? tags = null, CancellationToken cancellationToken = default) - { - var canBeCanceled = cancellationToken.CanBeCanceled; - if (canBeCanceled) - { - cancellationToken.ThrowIfCancellationRequested(); - } - - var flags = GetEffectiveFlags(options); - if ((flags & HybridCacheEntryFlags.DisableLocalCacheRead) == 0 && _localCache.TryGetValue(key, out var untyped) - && untyped is CacheItem typed && typed.TryGetValue(out var value)) - { - // short-circuit - return new(value); - } - - if (GetOrCreateStampedeState(key, flags, out var stampede, canBeCanceled)) - { - // new query; we're responsible for making it happen - if (canBeCanceled) - { - // *we* might cancel, but someone else might be depending on the result; start the - // work independently, then we'll with join the outcome - stampede.QueueUserWorkItem(in state, underlyingDataCallback, options); - } - else - { - // we're going to run to completion; no need to get complicated - _ = stampede.ExecuteDirectAsync(in state, underlyingDataCallback, options); // this larger task includes L2 write etc - return stampede.UnwrapReservedAsync(); - } - } - - return stampede.JoinAsync(cancellationToken); - } - - public override ValueTask RemoveAsync(string key, CancellationToken token = default) - { - _localCache.Remove(key); - return _backendCache is null ? default : new(_backendCache.RemoveAsync(key, token)); - } - - public override ValueTask RemoveByTagAsync(string tag, CancellationToken token = default) - => default; // tags not yet implemented - - public override ValueTask SetAsync(string key, T value, HybridCacheEntryOptions? options = null, IEnumerable? tags = null, CancellationToken token = default) - { - // since we're forcing a write: disable L1+L2 read; we'll use a direct pass-thru of the value as the callback, to reuse all the code; - // note also that stampede token is not shared with anyone else - var flags = GetEffectiveFlags(options) | (HybridCacheEntryFlags.DisableLocalCacheRead | HybridCacheEntryFlags.DisableDistributedCacheRead); - var state = new StampedeState(this, new StampedeKey(key, flags), token); - return new(state.ExecuteDirectAsync(value, static (state, _) => new(state), options)); // note this spans L2 write etc - } -} diff --git a/src/Caching/Hybrid/src/Internal/DefaultJsonSerializerFactory.cs b/src/Caching/Hybrid/src/Internal/DefaultJsonSerializerFactory.cs deleted file mode 100644 index 63ce186e1ec4..000000000000 --- a/src/Caching/Hybrid/src/Internal/DefaultJsonSerializerFactory.cs +++ /dev/null @@ -1,42 +0,0 @@ -// 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.Diagnostics.CodeAnalysis; -using System.Text.Json; - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; - -internal sealed class DefaultJsonSerializerFactory : IHybridCacheSerializerFactory -{ - public bool TryCreateSerializer([NotNullWhen(true)] out IHybridCacheSerializer? serializer) - { - // no restriction - serializer = new DefaultJsonSerializer(); - return true; - } - - internal sealed class DefaultJsonSerializer : IHybridCacheSerializer - { - T IHybridCacheSerializer.Deserialize(ReadOnlySequence source) - { - var reader = new Utf8JsonReader(source); -#pragma warning disable IDE0079 // unnecessary suppression: TFM-dependent -#pragma warning disable IL2026, IL3050 // AOT bits - return JsonSerializer.Deserialize(ref reader)!; -#pragma warning restore IL2026, IL3050 -#pragma warning restore IDE0079 - } - - void IHybridCacheSerializer.Serialize(T value, IBufferWriter target) - { - using var writer = new Utf8JsonWriter(target); -#pragma warning disable IDE0079 // unnecessary suppression: TFM-dependent -#pragma warning disable IL2026, IL3050 // AOT bits - JsonSerializer.Serialize(writer, value, JsonSerializerOptions.Default); -#pragma warning restore IL2026, IL3050 -#pragma warning restore IDE0079 - } - } - -} diff --git a/src/Caching/Hybrid/src/Internal/InbuiltTypeSerializer.cs b/src/Caching/Hybrid/src/Internal/InbuiltTypeSerializer.cs deleted file mode 100644 index a043fc1ca203..000000000000 --- a/src/Caching/Hybrid/src/Internal/InbuiltTypeSerializer.cs +++ /dev/null @@ -1,56 +0,0 @@ -// 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.Buffers; -using System.Diagnostics; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; - -internal sealed class InbuiltTypeSerializer : IHybridCacheSerializer, IHybridCacheSerializer -{ - public static InbuiltTypeSerializer Instance { get; } = new(); - - string IHybridCacheSerializer.Deserialize(ReadOnlySequence source) - { -#if NET5_0_OR_GREATER - return Encoding.UTF8.GetString(source); -#else - if (source.IsSingleSegment && MemoryMarshal.TryGetArray(source.First, out var segment)) - { - // we can use the existing single chunk as-is - return Encoding.UTF8.GetString(segment.Array, segment.Offset, segment.Count); - } - - var length = checked((int)source.Length); - var oversized = ArrayPool.Shared.Rent(length); - source.CopyTo(oversized); - var s = Encoding.UTF8.GetString(oversized, 0, length); - ArrayPool.Shared.Return(oversized); - return s; -#endif - } - - void IHybridCacheSerializer.Serialize(string value, IBufferWriter target) - { -#if NET5_0_OR_GREATER - Encoding.UTF8.GetBytes(value, target); -#else - var length = Encoding.UTF8.GetByteCount(value); - var oversized = ArrayPool.Shared.Rent(length); - var actual = Encoding.UTF8.GetBytes(value, 0, value.Length, oversized, 0); - Debug.Assert(actual == length); - target.Write(new(oversized, 0, length)); - ArrayPool.Shared.Return(oversized); -#endif - } - - byte[] IHybridCacheSerializer.Deserialize(ReadOnlySequence source) - => source.ToArray(); - - void IHybridCacheSerializer.Serialize(byte[] value, IBufferWriter target) - => target.Write(value); -} diff --git a/src/Caching/Hybrid/src/Internal/RecyclableArrayBufferWriter.cs b/src/Caching/Hybrid/src/Internal/RecyclableArrayBufferWriter.cs deleted file mode 100644 index 9c65a33863a5..000000000000 --- a/src/Caching/Hybrid/src/Internal/RecyclableArrayBufferWriter.cs +++ /dev/null @@ -1,200 +0,0 @@ -// 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.Buffers; -using System.Diagnostics; -using System.Threading; - -namespace Microsoft.Extensions.Caching.Hybrid.Internal; - -// this is effectively a cut-down re-implementation of ArrayBufferWriter -// from https://github.com/dotnet/runtime/blob/6cd9bf1937c3b4d2f7304a6c534aacde58a202b6/src/libraries/Common/src/System/Buffers/ArrayBufferWriter.cs -// except it uses the array pool for allocations -internal sealed class RecyclableArrayBufferWriter : IBufferWriter, IDisposable -{ - // Usage note: *normally* you might want to use "using" for this, and that is fine; - // however, caution should be exercised in exception scenarios where we don't 100% - // know that the caller has stopped touching the buffer; in particular, this means - // scenarios involving a combination of external code and (for example) "async". - // In those cases, it may be preferable to manually dispose in the success case, - // and just drop the buffers in the failure case, i.e. instead of: - // - // using (writer) - // { DoStuff(); } - // - // simply: - // - // DoStuff(); - // writer.Dispose(); - // - // This does not represent a problem, and is consistent with many ArrayPool use-cases. - - // Copy of Array.MaxLength. - // Used by projects targeting .NET Framework. - private const int ArrayMaxLength = 0x7FFFFFC7; - - private const int DefaultInitialBufferSize = 256; - - private T[] _buffer; - private int _index; - private int _maxLength; - - public int CommittedBytes => _index; - public int FreeCapacity => _buffer.Length - _index; - - private static RecyclableArrayBufferWriter? _spare; - public static RecyclableArrayBufferWriter Create(int maxLength) - { - var obj = Interlocked.Exchange(ref _spare, null) ?? new(); - Debug.Assert(obj._index == 0); - obj._maxLength = maxLength; - return obj; - } - - private RecyclableArrayBufferWriter() - { - _buffer = []; - _index = 0; - _maxLength = int.MaxValue; - } - - public void Dispose() - { - // attempt to reuse everything via "spare"; if that isn't possible, - // recycle the buffers instead - _index = 0; - if (Interlocked.CompareExchange(ref _spare, this, null) != null) - { - var tmp = _buffer; - _buffer = []; - if (tmp.Length != 0) - { - ArrayPool.Shared.Return(tmp); - } - } - } - - public void Advance(int count) - { - if (count < 0) - { - throw new ArgumentException(null, nameof(count)); - } - - if (_index > _buffer.Length - count) - { - ThrowCount(); - } - - if (_index + count > _maxLength) - { - ThrowQuota(); - } - - _index += count; - - static void ThrowCount() - => throw new ArgumentOutOfRangeException(nameof(count)); - - static void ThrowQuota() - => throw new InvalidOperationException("Max length exceeded"); - } - - /// - /// Disconnect the current buffer so that we can store it without it being recycled - /// - internal T[] DetachCommitted(out int length) - { - var tmp = _index == 0 ? [] : _buffer; - length = _index; - - _buffer = []; - _index = 0; - - return tmp; - } - - public void ResetInPlace() - { - // resets the writer *without* resetting the buffer; - // the existing memory should be considered "gone" - // (to claim the buffer instead, use DetachCommitted) - _index = 0; - } - - internal T[] GetBuffer(out int length) - { - length = _index; - return _index == 0 ? [] : _buffer; - } - - public ReadOnlyMemory GetCommittedMemory() => new(_buffer, 0, _index); // could also directly expose a ReadOnlySpan if useful - - public Memory GetMemory(int sizeHint = 0) - { - CheckAndResizeBuffer(sizeHint); - Debug.Assert(_buffer.Length > _index); - return _buffer.AsMemory(_index); - } - - public Span GetSpan(int sizeHint = 0) - { - CheckAndResizeBuffer(sizeHint); - Debug.Assert(_buffer.Length > _index); - return _buffer.AsSpan(_index); - } - - // create a standalone isolated copy of the buffer - public T[] ToArray() => _buffer.AsSpan(0, _index).ToArray(); - - private void CheckAndResizeBuffer(int sizeHint) - { - if (sizeHint <= 0) - { - sizeHint = 1; - } - - if (sizeHint > FreeCapacity) - { - var currentLength = _buffer.Length; - - // Attempt to grow by the larger of the sizeHint and double the current size. - var growBy = Math.Max(sizeHint, currentLength); - - if (currentLength == 0) - { - growBy = Math.Max(growBy, DefaultInitialBufferSize); - } - - var newSize = currentLength + growBy; - - if ((uint)newSize > int.MaxValue) - { - // Attempt to grow to ArrayMaxLength. - var needed = (uint)(currentLength - FreeCapacity + sizeHint); - Debug.Assert(needed > currentLength); - - if (needed > ArrayMaxLength) - { - ThrowOutOfMemoryException(); - } - - newSize = ArrayMaxLength; - } - - // resize the backing buffer - var oldArray = _buffer; - _buffer = ArrayPool.Shared.Rent(newSize); - oldArray.AsSpan(0, _index).CopyTo(_buffer); - if (oldArray.Length != 0) - { - ArrayPool.Shared.Return(oldArray); - } - } - - Debug.Assert(FreeCapacity > 0 && FreeCapacity >= sizeHint); - - static void ThrowOutOfMemoryException() => throw new InvalidOperationException("Unable to grow buffer as requested"); - } -} diff --git a/src/Caching/Hybrid/src/Internal/readme.md b/src/Caching/Hybrid/src/Internal/readme.md deleted file mode 100644 index 8d6a7d878481..000000000000 --- a/src/Caching/Hybrid/src/Internal/readme.md +++ /dev/null @@ -1,27 +0,0 @@ -# HybridCache internal design - -`HybridCache` encapsulates serialization, caching and stampede protection. - -The `DefaultHybridCache` implementation keeps a collection of `StampedeState` entries -that represent the current in-flight operations (keyed by `StampedeKey`); if a duplicate -operation occurs during the execution, the second operation will be joined with that -same flow, rather than executing independently. When attempting to merge with an -existing flow, interlocked counting is used: we can only join if we can successfully -increment the value from a non-zero value (zero meaning all existing consumers have -canceled, and the shared token is therefore canceled) - -The `StampedeState<>` performs back-end fetch operations, resulting not in a `T` (of the final -value), but instead a `CacheItem`; this is the object that gets put into L1 cache, -and can describe both mutable and immutable types; the significance here is that for -mutable types, we need a defensive copy per-call to prevent callers impacting each-other. - -`StampedeState<>` combines cancellation (so that operations proceed as long as *a* caller -is still active); this covers all L2 access and serialization operations, releasing all pending -shared callers for the same operation. Note that L2 storage can occur *after* callers -have been released. - -To ensure correct buffer recycling, when dealing with cache entries that need defensive copies -we use more ref-counting while reading the buffer, combined with an eviction callback which -decrements that counter. This means that we recycle things when evicted, without impacting -in-progress deserialize operations. To simplify tracking, `BufferChunk` acts like a `byte[]`+`int` -(we don't need non-zero offset), but also tracking "should this be returned to the pool?". diff --git a/src/Caching/Hybrid/src/Microsoft.Extensions.Caching.Hybrid.csproj b/src/Caching/Hybrid/src/Microsoft.Extensions.Caching.Hybrid.csproj deleted file mode 100644 index a4a09cdd4395..000000000000 --- a/src/Caching/Hybrid/src/Microsoft.Extensions.Caching.Hybrid.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - Multi-level caching implementation building on and extending IDistributedCache - - $(DefaultNetCoreTargetFramework);$(DefaultNetFxTargetFramework);netstandard2.0;netstandard2.1;$(CurrentLtsTargetFramework) - true - cache;distributedcache;hybrid - true - false - true - true - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Caching/Hybrid/src/PublicAPI.Shipped.txt b/src/Caching/Hybrid/src/PublicAPI.Shipped.txt deleted file mode 100644 index ab058de62d44..000000000000 --- a/src/Caching/Hybrid/src/PublicAPI.Shipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable diff --git a/src/Caching/Hybrid/src/PublicAPI.Unshipped.txt b/src/Caching/Hybrid/src/PublicAPI.Unshipped.txt deleted file mode 100644 index abe172bda56a..000000000000 --- a/src/Caching/Hybrid/src/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,23 +0,0 @@ -#nullable enable -Microsoft.Extensions.Caching.Hybrid.HybridCacheOptions -Microsoft.Extensions.Caching.Hybrid.HybridCacheOptions.DefaultEntryOptions.get -> Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryOptions? -Microsoft.Extensions.Caching.Hybrid.HybridCacheOptions.DefaultEntryOptions.set -> void -Microsoft.Extensions.Caching.Hybrid.HybridCacheOptions.DisableCompression.get -> bool -Microsoft.Extensions.Caching.Hybrid.HybridCacheOptions.DisableCompression.set -> void -Microsoft.Extensions.Caching.Hybrid.HybridCacheOptions.HybridCacheOptions() -> void -Microsoft.Extensions.Caching.Hybrid.HybridCacheOptions.MaximumKeyLength.get -> int -Microsoft.Extensions.Caching.Hybrid.HybridCacheOptions.MaximumKeyLength.set -> void -Microsoft.Extensions.Caching.Hybrid.HybridCacheOptions.MaximumPayloadBytes.get -> long -Microsoft.Extensions.Caching.Hybrid.HybridCacheOptions.MaximumPayloadBytes.set -> void -Microsoft.Extensions.Caching.Hybrid.HybridCacheOptions.ReportTagMetrics.get -> bool -Microsoft.Extensions.Caching.Hybrid.HybridCacheOptions.ReportTagMetrics.set -> void -Microsoft.Extensions.Caching.Hybrid.IHybridCacheBuilder -Microsoft.Extensions.Caching.Hybrid.IHybridCacheBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -Microsoft.Extensions.DependencyInjection.HybridCacheBuilderExtensions -Microsoft.Extensions.DependencyInjection.HybridCacheServiceExtensions -static Microsoft.Extensions.DependencyInjection.HybridCacheBuilderExtensions.AddSerializer(this Microsoft.Extensions.Caching.Hybrid.IHybridCacheBuilder! builder) -> Microsoft.Extensions.Caching.Hybrid.IHybridCacheBuilder! -static Microsoft.Extensions.DependencyInjection.HybridCacheBuilderExtensions.AddSerializer(this Microsoft.Extensions.Caching.Hybrid.IHybridCacheBuilder! builder, Microsoft.Extensions.Caching.Hybrid.IHybridCacheSerializer! serializer) -> Microsoft.Extensions.Caching.Hybrid.IHybridCacheBuilder! -static Microsoft.Extensions.DependencyInjection.HybridCacheBuilderExtensions.AddSerializerFactory(this Microsoft.Extensions.Caching.Hybrid.IHybridCacheBuilder! builder, Microsoft.Extensions.Caching.Hybrid.IHybridCacheSerializerFactory! factory) -> Microsoft.Extensions.Caching.Hybrid.IHybridCacheBuilder! -static Microsoft.Extensions.DependencyInjection.HybridCacheBuilderExtensions.AddSerializerFactory(this Microsoft.Extensions.Caching.Hybrid.IHybridCacheBuilder! builder) -> Microsoft.Extensions.Caching.Hybrid.IHybridCacheBuilder! -static Microsoft.Extensions.DependencyInjection.HybridCacheServiceExtensions.AddHybridCache(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.Caching.Hybrid.IHybridCacheBuilder! -static Microsoft.Extensions.DependencyInjection.HybridCacheServiceExtensions.AddHybridCache(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! setupAction) -> Microsoft.Extensions.Caching.Hybrid.IHybridCacheBuilder! diff --git a/src/Caching/Hybrid/test/BasicConfig.json b/src/Caching/Hybrid/test/BasicConfig.json deleted file mode 100644 index 374114fb1dba..000000000000 --- a/src/Caching/Hybrid/test/BasicConfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "no_entry_options": { - "MaximumKeyLength": 937 - }, - "with_entry_options": { - "MaximumKeyLength": 937, - "DefaultEntryOptions": { - "LocalCacheExpiration": "00:02:00", - "Flags": "DisableCompression,DisableLocalCacheRead" - } - } -} diff --git a/src/Caching/Hybrid/test/BufferReleaseTests.cs b/src/Caching/Hybrid/test/BufferReleaseTests.cs deleted file mode 100644 index 046cb23d0762..000000000000 --- a/src/Caching/Hybrid/test/BufferReleaseTests.cs +++ /dev/null @@ -1,227 +0,0 @@ -// 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.Runtime.CompilerServices; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Hybrid.Internal; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using static Microsoft.Extensions.Caching.Hybrid.Internal.DefaultHybridCache; - -namespace Microsoft.Extensions.Caching.Hybrid.Tests; - -public class BufferReleaseTests // note that buffer ref-counting is only enabled for DEBUG builds; can only verify general behaviour without that -{ - static ServiceProvider GetDefaultCache(out DefaultHybridCache cache, Action? config = null) - { - var services = new ServiceCollection(); - config?.Invoke(services); - services.AddHybridCache(); - var provider = services.BuildServiceProvider(); - cache = Assert.IsType(provider.GetRequiredService()); - return provider; - } - - [Fact] - public async Task BufferGetsReleased_NoL2() - { - using var provider = GetDefaultCache(out var cache); -#if DEBUG - cache.DebugOnlyGetOutstandingBuffers(flush: true); -#endif - - var key = Me(); -#if DEBUG - Assert.Equal(0, cache.DebugOnlyGetOutstandingBuffers()); -#endif - var first = await cache.GetOrCreateAsync(key, _ => GetAsync()); - Assert.NotNull(first); -#if DEBUG - Assert.Equal(1, cache.DebugOnlyGetOutstandingBuffers()); -#endif - Assert.True(cache.DebugTryGetCacheItem(key, out var cacheItem)); - - // assert that we can reserve the buffer *now* (mostly to see that it behaves differently later) - Assert.True(cacheItem.NeedsEvictionCallback, "should be pooled memory"); - Assert.True(cacheItem.TryReserveBuffer(out _)); - cacheItem.Release(); // for the above reserve - - var second = await cache.GetOrCreateAsync(key, _ => GetAsync(), _noUnderlying); - Assert.NotNull(second); - Assert.NotSame(first, second); - - Assert.Equal(1, cacheItem.RefCount); - await cache.RemoveAsync(key); - var third = await cache.GetOrCreateAsync(key, _ => GetAsync(), _noUnderlying); - Assert.Null(third); - - // give it a moment for the eviction callback to kick in - for (var i = 0; i < 10 && cacheItem.NeedsEvictionCallback; i++) - { - await Task.Delay(250); - } -#if DEBUG - Assert.Equal(0, cache.DebugOnlyGetOutstandingBuffers()); -#endif - // assert that we can *no longer* reserve this buffer, because we've already recycled it - Assert.False(cacheItem.TryReserveBuffer(out _)); - Assert.Equal(0, cacheItem.RefCount); - Assert.False(cacheItem.NeedsEvictionCallback, "should be recycled now"); - static ValueTask GetAsync() => new(new Customer { Id = 42, Name = "Fred" }); - } - - private static readonly HybridCacheEntryOptions _noUnderlying = new() { Flags = HybridCacheEntryFlags.DisableUnderlyingData }; - - class TestCache : MemoryDistributedCache, IBufferDistributedCache - { - public TestCache(IOptions options) : base(options) { } - - void IBufferDistributedCache.Set(string key, ReadOnlySequence value, DistributedCacheEntryOptions options) - => Set(key, value.ToArray(), options); // efficiency not important for this - - ValueTask IBufferDistributedCache.SetAsync(string key, ReadOnlySequence value, DistributedCacheEntryOptions options, CancellationToken token) - => new(SetAsync(key, value.ToArray(), options, token)); // efficiency not important for this - - bool IBufferDistributedCache.TryGet(string key, IBufferWriter destination) - => Write(destination, Get(key)); - - async ValueTask IBufferDistributedCache.TryGetAsync(string key, IBufferWriter destination, CancellationToken token) - => Write(destination, await GetAsync(key, token)); - - static bool Write(IBufferWriter destination, byte[]? buffer) - { - if (buffer is null) - { - return false; - } - destination.Write(buffer); - return true; - } - } - - [Fact] - public async Task BufferDoesNotNeedRelease_LegacyL2() // byte[] API; not pooled - { - using var provider = GetDefaultCache(out var cache, - services => services.AddSingleton()); - - cache.DebugRemoveFeatures(CacheFeatures.BackendBuffers); - // prep the backend with our data - var key = Me(); - Assert.NotNull(cache.BackendCache); - var serializer = cache.GetSerializer(); - using (var writer = RecyclableArrayBufferWriter.Create(int.MaxValue)) - { - serializer.Serialize(await GetAsync(), writer); - cache.BackendCache.Set(key, writer.ToArray()); - } -#if DEBUG - cache.DebugOnlyGetOutstandingBuffers(flush: true); - Assert.Equal(0, cache.DebugOnlyGetOutstandingBuffers()); -#endif - var first = await cache.GetOrCreateAsync(key, _ => GetAsync(), _noUnderlying); // we expect this to come from L2, hence NoUnderlying - Assert.NotNull(first); -#if DEBUG - Assert.Equal(0, cache.DebugOnlyGetOutstandingBuffers()); -#endif - Assert.True(cache.DebugTryGetCacheItem(key, out var cacheItem)); - - // assert that we can reserve the buffer *now* (mostly to see that it behaves differently later) - Assert.False(cacheItem.NeedsEvictionCallback, "should NOT be pooled memory"); - Assert.True(cacheItem.TryReserveBuffer(out _)); - cacheItem.Release(); // for the above reserve - - var second = await cache.GetOrCreateAsync(key, _ => GetAsync(), _noUnderlying); - Assert.NotNull(second); - Assert.NotSame(first, second); - - Assert.Equal(1, cacheItem.RefCount); - await cache.RemoveAsync(key); - var third = await cache.GetOrCreateAsync(key, _ => GetAsync(), _noUnderlying); - Assert.Null(third); - Assert.Null(await cache.BackendCache.GetAsync(key)); // should be gone from L2 too - - // give it a moment for the eviction callback to kick in - for (var i = 0; i < 10 && cacheItem.NeedsEvictionCallback; i++) - { - await Task.Delay(250); - } -#if DEBUG - Assert.Equal(0, cache.DebugOnlyGetOutstandingBuffers()); -#endif - // assert that we can *no longer* reserve this buffer, because we've already recycled it - Assert.True(cacheItem.TryReserveBuffer(out _)); // always readable - cacheItem.Release(); - Assert.Equal(1, cacheItem.RefCount); // not decremented because there was no need to add the hook - - Assert.False(cacheItem.NeedsEvictionCallback, "should still not need recycling"); - static ValueTask GetAsync() => new(new Customer { Id = 42, Name = "Fred" }); - } - - [Fact] - public async Task BufferGetsReleased_BufferL2() // IBufferWriter API; pooled - { - using var provider = GetDefaultCache(out var cache, - services => services.AddSingleton()); - - // prep the backend with our data - var key = Me(); - Assert.NotNull(cache.BackendCache); - var serializer = cache.GetSerializer(); - using (var writer = RecyclableArrayBufferWriter.Create(int.MaxValue)) - { - serializer.Serialize(await GetAsync(), writer); - cache.BackendCache.Set(key, writer.ToArray()); - } -#if DEBUG - cache.DebugOnlyGetOutstandingBuffers(flush: true); - Assert.Equal(0, cache.DebugOnlyGetOutstandingBuffers()); -#endif - var first = await cache.GetOrCreateAsync(key, _ => GetAsync(), _noUnderlying); // we expect this to come from L2, hence NoUnderlying - Assert.NotNull(first); -#if DEBUG - Assert.Equal(1, cache.DebugOnlyGetOutstandingBuffers()); -#endif - Assert.True(cache.DebugTryGetCacheItem(key, out var cacheItem)); - - // assert that we can reserve the buffer *now* (mostly to see that it behaves differently later) - Assert.True(cacheItem.NeedsEvictionCallback, "should be pooled memory"); - Assert.True(cacheItem.TryReserveBuffer(out _)); - cacheItem.Release(); // for the above reserve - - var second = await cache.GetOrCreateAsync(key, _ => GetAsync(), _noUnderlying); - Assert.NotNull(second); - Assert.NotSame(first, second); - - Assert.Equal(1, cacheItem.RefCount); - await cache.RemoveAsync(key); - var third = await cache.GetOrCreateAsync(key, _ => GetAsync(), _noUnderlying); - Assert.Null(third); - Assert.Null(await cache.BackendCache.GetAsync(key)); // should be gone from L2 too - - // give it a moment for the eviction callback to kick in - for (var i = 0; i < 10 && cacheItem.NeedsEvictionCallback; i++) - { - await Task.Delay(250); - } -#if DEBUG - Assert.Equal(0, cache.DebugOnlyGetOutstandingBuffers()); -#endif - // assert that we can *no longer* reserve this buffer, because we've already recycled it - Assert.False(cacheItem.TryReserveBuffer(out _)); // released now - Assert.Equal(0, cacheItem.RefCount); - - Assert.False(cacheItem.NeedsEvictionCallback, "should be recycled by now"); - static ValueTask GetAsync() => new(new Customer { Id = 42, Name = "Fred" }); - } - - public class Customer - { - public int Id { get; set; } - public string Name { get; set; } = ""; - } - - private static string Me([CallerMemberName] string caller = "") => caller; -} diff --git a/src/Caching/Hybrid/test/DistributedCacheTests.cs b/src/Caching/Hybrid/test/DistributedCacheTests.cs deleted file mode 100644 index bd7a5e58049e..000000000000 --- a/src/Caching/Hybrid/test/DistributedCacheTests.cs +++ /dev/null @@ -1,381 +0,0 @@ -// 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.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Hybrid.Internal; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Internal; -using Xunit.Abstractions; - -namespace Microsoft.Extensions.Caching.Hybrid.Tests; - -/// -/// Validate over-arching expectations of DC implementations, in particular behaviour re IBufferDistributedCache added for HybridCache -/// -public abstract class DistributedCacheTests -{ - public DistributedCacheTests(ITestOutputHelper log) => Log = log; - protected ITestOutputHelper Log { get; } - protected abstract ValueTask ConfigureAsync(IServiceCollection services); - protected abstract bool CustomClockSupported { get; } - - protected FakeTime Clock { get; } = new(); - - protected class FakeTime : TimeProvider, ISystemClock - { - private DateTimeOffset _now = DateTimeOffset.UtcNow; - public void Reset() => _now = DateTimeOffset.UtcNow; - - DateTimeOffset ISystemClock.UtcNow => _now; - - public override DateTimeOffset GetUtcNow() => _now; - - public void Add(TimeSpan delta) => _now += delta; - } - - private async ValueTask InitAsync() - { - Clock.Reset(); - var services = new ServiceCollection(); - services.AddSingleton(Clock); - services.AddSingleton(Clock); - await ConfigureAsync(services); - return services; - } - - [Theory] - [InlineData(0)] - [InlineData(128)] - [InlineData(1024)] - [InlineData(16 * 1024)] - public async Task SimpleBufferRoundtrip(int size) - { - var cache = (await InitAsync()).BuildServiceProvider().GetService(); - if (cache is null) - { - Log.WriteLine("Cache is not available"); - return; // inconclusive - } - - var key = $"{Me()}:{size}"; - cache.Remove(key); - Assert.Null(cache.Get(key)); - - var expected = new byte[size]; - new Random().NextBytes(expected); - cache.Set(key, expected, _fiveMinutes); - - var actual = cache.Get(key); - Assert.NotNull(actual); - Assert.True(expected.SequenceEqual(actual)); - Log.WriteLine("Data validated"); - - if (CustomClockSupported) - { - Clock.Add(TimeSpan.FromMinutes(4)); - actual = cache.Get(key); - Assert.NotNull(actual); - Assert.True(expected.SequenceEqual(actual)); - - Clock.Add(TimeSpan.FromMinutes(2)); - actual = cache.Get(key); - Assert.Null(actual); - - Log.WriteLine("Expiration validated"); - } - else - { - Log.WriteLine("Expiration not validated - TimeProvider not supported"); - } - } - - [Theory] - [InlineData(0)] - [InlineData(128)] - [InlineData(1024)] - [InlineData(16 * 1024)] - public async Task SimpleBufferRoundtripAsync(int size) - { - var cache = (await InitAsync()).BuildServiceProvider().GetService(); - if (cache is null) - { - Log.WriteLine("Cache is not available"); - return; // inconclusive - } - - var key = $"{Me()}:{size}"; - await cache.RemoveAsync(key); - Assert.Null(cache.Get(key)); - - var expected = new byte[size]; - new Random().NextBytes(expected); - await cache.SetAsync(key, expected, _fiveMinutes); - - var actual = await cache.GetAsync(key); - Assert.NotNull(actual); - Assert.True(expected.SequenceEqual(actual)); - Log.WriteLine("Data validated"); - - if (CustomClockSupported) - { - Clock.Add(TimeSpan.FromMinutes(4)); - actual = await cache.GetAsync(key); - Assert.NotNull(actual); - Assert.True(expected.SequenceEqual(actual)); - - Clock.Add(TimeSpan.FromMinutes(2)); - actual = await cache.GetAsync(key); - Assert.Null(actual); - - Log.WriteLine("Expiration validated"); - } - else - { - Log.WriteLine("Expiration not validated - TimeProvider not supported"); - } - } - - public enum SequenceKind - { - FullArray, - PaddedArray, - CustomMemory, - MultiSegment, - } - - [Theory] - [InlineData(0, SequenceKind.FullArray)] - [InlineData(128, SequenceKind.FullArray)] - [InlineData(1024, SequenceKind.FullArray)] - [InlineData(16 * 1024, SequenceKind.FullArray)] - [InlineData(0, SequenceKind.PaddedArray)] - [InlineData(128, SequenceKind.PaddedArray)] - [InlineData(1024, SequenceKind.PaddedArray)] - [InlineData(16 * 1024, SequenceKind.PaddedArray)] - [InlineData(0, SequenceKind.CustomMemory)] - [InlineData(128, SequenceKind.CustomMemory)] - [InlineData(1024, SequenceKind.CustomMemory)] - [InlineData(16 * 1024, SequenceKind.CustomMemory)] - [InlineData(0, SequenceKind.MultiSegment)] - [InlineData(128, SequenceKind.MultiSegment)] - [InlineData(1024, SequenceKind.MultiSegment)] - [InlineData(16 * 1024, SequenceKind.MultiSegment)] - public async Task ReadOnlySequenceBufferRoundtrip(int size, SequenceKind kind) - { - var cache = (await InitAsync()).BuildServiceProvider().GetService() as IBufferDistributedCache; - if (cache is null) - { - Log.WriteLine("Cache is not available or does not support IBufferDistributedCache"); - return; // inconclusive - } - - var key = $"{Me()}:{size}/{kind}"; - cache.Remove(key); - Assert.Null(cache.Get(key)); - - var payload = Invent(size, kind); - ReadOnlyMemory expected = payload.ToArray(); // simplify for testing - Assert.Equal(size, expected.Length); - cache.Set(key, payload, _fiveMinutes); - - var writer = RecyclableArrayBufferWriter.Create(int.MaxValue); - Assert.True(cache.TryGet(key, writer)); - Assert.True(expected.Span.SequenceEqual(writer.GetCommittedMemory().Span)); - writer.ResetInPlace(); - Log.WriteLine("Data validated"); - - if (CustomClockSupported) - { - Clock.Add(TimeSpan.FromMinutes(4)); - Assert.True(cache.TryGet(key, writer)); - Assert.True(expected.Span.SequenceEqual(writer.GetCommittedMemory().Span)); - writer.ResetInPlace(); - - Clock.Add(TimeSpan.FromMinutes(2)); - Assert.False(cache.TryGet(key, writer)); - Assert.Equal(0, writer.CommittedBytes); - - Log.WriteLine("Expiration validated"); - } - else - { - Log.WriteLine("Expiration not validated - TimeProvider not supported"); - } - } - - [Theory] - [InlineData(0, SequenceKind.FullArray)] - [InlineData(128, SequenceKind.FullArray)] - [InlineData(1024, SequenceKind.FullArray)] - [InlineData(16 * 1024, SequenceKind.FullArray)] - [InlineData(0, SequenceKind.PaddedArray)] - [InlineData(128, SequenceKind.PaddedArray)] - [InlineData(1024, SequenceKind.PaddedArray)] - [InlineData(16 * 1024, SequenceKind.PaddedArray)] - [InlineData(0, SequenceKind.CustomMemory)] - [InlineData(128, SequenceKind.CustomMemory)] - [InlineData(1024, SequenceKind.CustomMemory)] - [InlineData(16 * 1024, SequenceKind.CustomMemory)] - [InlineData(0, SequenceKind.MultiSegment)] - [InlineData(128, SequenceKind.MultiSegment)] - [InlineData(1024, SequenceKind.MultiSegment)] - [InlineData(16 * 1024, SequenceKind.MultiSegment)] - public async Task ReadOnlySequenceBufferRoundtripAsync(int size, SequenceKind kind) - { - var cache = (await InitAsync()).BuildServiceProvider().GetService() as IBufferDistributedCache; - if (cache is null) - { - Log.WriteLine("Cache is not available or does not support IBufferDistributedCache"); - return; // inconclusive - } - - var key = $"{Me()}:{size}/{kind}"; - await cache.RemoveAsync(key); - Assert.Null(await cache.GetAsync(key)); - - var payload = Invent(size, kind); - ReadOnlyMemory expected = payload.ToArray(); // simplify for testing - Assert.Equal(size, expected.Length); - await cache.SetAsync(key, payload, _fiveMinutes); - - var writer = RecyclableArrayBufferWriter.Create(int.MaxValue); - Assert.True(await cache.TryGetAsync(key, writer)); - Assert.True(expected.Span.SequenceEqual(writer.GetCommittedMemory().Span)); - writer.ResetInPlace(); - Log.WriteLine("Data validated"); - - if (CustomClockSupported) - { - Clock.Add(TimeSpan.FromMinutes(4)); - Assert.True(await cache.TryGetAsync(key, writer)); - Assert.True(expected.Span.SequenceEqual(writer.GetCommittedMemory().Span)); - writer.ResetInPlace(); - - Clock.Add(TimeSpan.FromMinutes(2)); - Assert.False(await cache.TryGetAsync(key, writer)); - Assert.Equal(0, writer.CommittedBytes); - - Log.WriteLine("Expiration validated"); - } - else - { - Log.WriteLine("Expiration not validated - TimeProvider not supported"); - } - } - - static ReadOnlySequence Invent(int size, SequenceKind kind) - { - var rand = new Random(); - ReadOnlySequence payload; - switch (kind) - { - case SequenceKind.FullArray: - var arr = new byte[size]; - rand.NextBytes(arr); - payload = new(arr); - break; - case SequenceKind.PaddedArray: - arr = new byte[size + 10]; - rand.NextBytes(arr); - payload = new(arr, 5, arr.Length - 10); - break; - case SequenceKind.CustomMemory: - var mem = new CustomMemory(size, rand).Memory; - payload = new(mem); - break; - case SequenceKind.MultiSegment: - if (size == 0) - { - payload = default; - break; - } - if (size < 10) - { - throw new ArgumentException("small segments not considered"); // a pain to construct - } - CustomSegment first = new(10, rand, null), // we'll take the last 3 of this 10 - second = new(size - 7, rand, first), // we'll take all of this one - third = new(10, rand, second); // we'll take the first 4 of this 10 - payload = new(first, 7, third, 4); - break; - default: - throw new ArgumentOutOfRangeException(nameof(kind)); - } - - // now validate what we expect of that payload - Assert.Equal(size, payload.Length); - switch (kind) - { - case SequenceKind.CustomMemory or SequenceKind.MultiSegment when size == 0: - Assert.True(payload.IsSingleSegment); - Assert.True(MemoryMarshal.TryGetArray(payload.First, out _)); - break; - case SequenceKind.MultiSegment: - Assert.False(payload.IsSingleSegment); - break; - case SequenceKind.CustomMemory: - Assert.True(payload.IsSingleSegment); - Assert.False(MemoryMarshal.TryGetArray(payload.First, out _)); - break; - case SequenceKind.FullArray: - Assert.True(payload.IsSingleSegment); - Assert.True(MemoryMarshal.TryGetArray(payload.First, out var segment)); - Assert.Equal(0, segment.Offset); - Assert.NotNull(segment.Array); - Assert.Equal(size, segment.Count); - Assert.Equal(size, segment.Array.Length); - break; - case SequenceKind.PaddedArray: - Assert.True(payload.IsSingleSegment); - Assert.True(MemoryMarshal.TryGetArray(payload.First, out segment)); - Assert.NotEqual(0, segment.Offset); - Assert.NotNull(segment.Array); - Assert.Equal(size, segment.Count); - Assert.NotEqual(size, segment.Array.Length); - break; - } - return payload; - } - - class CustomSegment : ReadOnlySequenceSegment - { - public CustomSegment(int size, Random? rand, CustomSegment? previous) - { - var arr = new byte[size + 10]; - rand?.NextBytes(arr); - Memory = new(arr, 5, arr.Length - 10); - if (previous is not null) - { - RunningIndex = previous.RunningIndex + previous.Memory.Length; - previous.Next = this; - } - } - } - - class CustomMemory : MemoryManager - { - private readonly byte[] _data; - public CustomMemory(int size, Random? rand = null) - { - _data = new byte[size + 10]; - rand?.NextBytes(_data); - } - public override Span GetSpan() => new(_data, 5, _data.Length - 10); - public override MemoryHandle Pin(int elementIndex = 0) => throw new NotSupportedException(); - public override void Unpin() => throw new NotSupportedException(); - protected override void Dispose(bool disposing) { } - protected override bool TryGetArray(out ArraySegment segment) - { - segment = default; - return false; - } - } - - private static readonly DistributedCacheEntryOptions _fiveMinutes - = new() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }; - - protected static string Me([CallerMemberName] string caller = "") => caller; -} diff --git a/src/Caching/Hybrid/test/FunctionalTests.cs b/src/Caching/Hybrid/test/FunctionalTests.cs deleted file mode 100644 index 65993939aa1e..000000000000 --- a/src/Caching/Hybrid/test/FunctionalTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.CompilerServices; -using Microsoft.Extensions.Caching.Hybrid.Internal; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.Caching.Hybrid.Tests; -public class FunctionalTests -{ - static ServiceProvider GetDefaultCache(out DefaultHybridCache cache, Action? config = null) - { - var services = new ServiceCollection(); - config?.Invoke(services); - services.AddHybridCache(); - var provider = services.BuildServiceProvider(); - cache = Assert.IsType(provider.GetRequiredService()); - return provider; - } - - [Fact] - public async Task RemoveSingleKey() - { - using var provider = GetDefaultCache(out var cache); - var key = Me(); - Assert.Equal(42, await cache.GetOrCreateAsync(key, _ => new ValueTask(42))); - - // now slightly different func to show delta; should use cached value initially - await cache.RemoveAsync("unrelated"); - Assert.Equal(42, await cache.GetOrCreateAsync(key, _ => new ValueTask(96))); - - // now remove and repeat - should get updated value - await cache.RemoveAsync(key); - Assert.Equal(96, await cache.GetOrCreateAsync(key, _ => new ValueTask(96))); - } - - [Fact] - public async Task RemoveNoKeyViaArray() - { - using var provider = GetDefaultCache(out var cache); - var key = Me(); - Assert.Equal(42, await cache.GetOrCreateAsync(key, _ => new ValueTask(42))); - - // now slightly different func to show delta; should use same cached value - await cache.RemoveAsync([]); - Assert.Equal(42, await cache.GetOrCreateAsync(key, _ => new ValueTask(96))); - } - - [Fact] - public async Task RemoveSingleKeyViaArray() - { - using var provider = GetDefaultCache(out var cache); - var key = Me(); - Assert.Equal(42, await cache.GetOrCreateAsync(key, _ => new ValueTask(42))); - - // now slightly different func to show delta; should use cached value initially - await cache.RemoveAsync(["unrelated"]); - Assert.Equal(42, await cache.GetOrCreateAsync(key, _ => new ValueTask(96))); - - // now remove and repeat - should get updated value - await cache.RemoveAsync([key]); - Assert.Equal(96, await cache.GetOrCreateAsync(key, _ => new ValueTask(96))); - } - - [Fact] - public async Task RemoveMultipleKeysViaArray() - { - using var provider = GetDefaultCache(out var cache); - var key = Me(); - Assert.Equal(42, await cache.GetOrCreateAsync(key, _ => new ValueTask(42))); - - // now slightly different func to show delta; should use cached value initially - Assert.Equal(42, await cache.GetOrCreateAsync(key, _ => new ValueTask(96))); - - // now remove and repeat - should get updated value - await cache.RemoveAsync([key, "unrelated"]); - Assert.Equal(96, await cache.GetOrCreateAsync(key, _ => new ValueTask(96))); - } - - private static string Me([CallerMemberName] string caller = "") => caller; - -} diff --git a/src/Caching/Hybrid/test/L2Tests.cs b/src/Caching/Hybrid/test/L2Tests.cs deleted file mode 100644 index 378e5c8ed4ba..000000000000 --- a/src/Caching/Hybrid/test/L2Tests.cs +++ /dev/null @@ -1,261 +0,0 @@ -// 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.Collections; -using System.Runtime.CompilerServices; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Hybrid.Internal; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Xunit.Abstractions; - -namespace Microsoft.Extensions.Caching.Hybrid.Tests; -public class L2Tests(ITestOutputHelper Log) -{ - class Options(T Value) : IOptions where T : class - { - T IOptions.Value => Value; - } - ServiceProvider GetDefaultCache(bool buffers, out DefaultHybridCache cache) - { - var services = new ServiceCollection(); - var localCacheOptions = new Options(new()); - var localCache = new MemoryDistributedCache(localCacheOptions); - services.AddSingleton(buffers ? new BufferLoggingCache(Log, localCache) : new LoggingCache(Log, localCache)); - services.AddHybridCache(); - var provider = services.BuildServiceProvider(); - cache = Assert.IsType(provider.GetRequiredService()); - return provider; - } - - static string CreateString(bool work = false) - { - Assert.True(work, "we didn't expect this to be invoked"); - return Guid.NewGuid().ToString(); - } - - private static readonly HybridCacheEntryOptions Expiry = new() { Expiration = TimeSpan.FromMinutes(3.5) }; - - private static readonly HybridCacheEntryOptions ExpiryNoL1 = new() { Flags = HybridCacheEntryFlags.DisableLocalCache, Expiration = TimeSpan.FromMinutes(3.5) }; - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task AssertL2Operations_Immutable(bool buffers) - { - using var provider = GetDefaultCache(buffers, out var cache); - var backend = Assert.IsAssignableFrom(cache.BackendCache); - Log.WriteLine("Inventing key..."); - var s = await cache.GetOrCreateAsync(Me(), ct => new ValueTask(CreateString(true))); - Assert.Equal(2, backend.OpCount); // GET, SET - - Log.WriteLine("Reading with L1..."); - for (var i = 0; i < 5; i++) - { - var x = await cache.GetOrCreateAsync(Me(), ct => new ValueTask(CreateString())); - Assert.Equal(s, x); - Assert.Same(s, x); - } - Assert.Equal(2, backend.OpCount); // shouldn't be hit - - Log.WriteLine("Reading without L1..."); - for (var i = 0; i < 5; i++) - { - var x = await cache.GetOrCreateAsync(Me(), ct => new ValueTask(CreateString()), ExpiryNoL1); - Assert.Equal(s, x); - Assert.NotSame(s, x); - } - Assert.Equal(7, backend.OpCount); // should be read every time - - Log.WriteLine("Setting value directly"); - s = CreateString(true); - await cache.SetAsync(Me(), s); - for (var i = 0; i < 5; i++) - { - var x = await cache.GetOrCreateAsync(Me(), ct => new ValueTask(CreateString())); - Assert.Equal(s, x); - Assert.Same(s, x); - } - Assert.Equal(8, backend.OpCount); // SET - - Log.WriteLine("Removing key..."); - await cache.RemoveAsync(Me()); - Assert.Equal(9, backend.OpCount); // DEL - - Log.WriteLine("Fetching new..."); - var t = await cache.GetOrCreateAsync(Me(), ct => new ValueTask(CreateString(true))); - Assert.NotEqual(s, t); - Assert.Equal(11, backend.OpCount); // GET, SET - } - - public sealed class Foo - { - public string Value { get; set; } = ""; - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task AssertL2Operations_Mutable(bool buffers) - { - using var provider = GetDefaultCache(buffers, out var cache); - var backend = Assert.IsAssignableFrom(cache.BackendCache); - Log.WriteLine("Inventing key..."); - var s = await cache.GetOrCreateAsync(Me(), ct => new ValueTask(new Foo { Value = CreateString(true) }), Expiry); - Assert.Equal(2, backend.OpCount); // GET, SET - - Log.WriteLine("Reading with L1..."); - for (var i = 0; i < 5; i++) - { - var x = await cache.GetOrCreateAsync(Me(), ct => new ValueTask(new Foo { Value = CreateString() }), Expiry); - Assert.Equal(s.Value, x.Value); - Assert.NotSame(s, x); - } - Assert.Equal(2, backend.OpCount); // shouldn't be hit - - Log.WriteLine("Reading without L1..."); - for (var i = 0; i < 5; i++) - { - var x = await cache.GetOrCreateAsync(Me(), ct => new ValueTask(new Foo { Value = CreateString() }), ExpiryNoL1); - Assert.Equal(s.Value, x.Value); - Assert.NotSame(s, x); - } - Assert.Equal(7, backend.OpCount); // should be read every time - - Log.WriteLine("Setting value directly"); - s = new Foo { Value = CreateString(true) }; - await cache.SetAsync(Me(), s); - for (var i = 0; i < 5; i++) - { - var x = await cache.GetOrCreateAsync(Me(), ct => new ValueTask(new Foo { Value = CreateString() }), Expiry); - Assert.Equal(s.Value, x.Value); - Assert.NotSame(s, x); - } - Assert.Equal(8, backend.OpCount); // SET - - Log.WriteLine("Removing key..."); - await cache.RemoveAsync(Me()); - Assert.Equal(9, backend.OpCount); // DEL - - Log.WriteLine("Fetching new..."); - var t = await cache.GetOrCreateAsync(Me(), ct => new ValueTask(new Foo { Value = CreateString(true) }), Expiry); - Assert.NotEqual(s.Value, t.Value); - Assert.Equal(11, backend.OpCount); // GET, SET - } - - class BufferLoggingCache : LoggingCache, IBufferDistributedCache - { - public BufferLoggingCache(ITestOutputHelper log, IDistributedCache tail) : base(log, tail) { } - - void IBufferDistributedCache.Set(string key, ReadOnlySequence value, DistributedCacheEntryOptions options) - { - Interlocked.Increment(ref opcount); - Log.WriteLine($"Set (ROS-byte): {key}"); - Tail.Set(key, value.ToArray(), options); - } - - ValueTask IBufferDistributedCache.SetAsync(string key, ReadOnlySequence value, DistributedCacheEntryOptions options, CancellationToken token) - { - Interlocked.Increment(ref opcount); - Log.WriteLine($"SetAsync (ROS-byte): {key}"); - return new(Tail.SetAsync(key, value.ToArray(), options, token)); - } - - bool IBufferDistributedCache.TryGet(string key, IBufferWriter destination) - { - Interlocked.Increment(ref opcount); - Log.WriteLine($"TryGet: {key}"); - var buffer = Tail.Get(key); - if (buffer is null) - { - return false; - } - destination.Write(buffer); - return true; - } - - async ValueTask IBufferDistributedCache.TryGetAsync(string key, IBufferWriter destination, CancellationToken token) - { - Interlocked.Increment(ref opcount); - Log.WriteLine($"TryGetAsync: {key}"); - var buffer = await Tail.GetAsync(key, token); - if (buffer is null) - { - return false; - } - destination.Write(buffer); - return true; - } - } - - class LoggingCache(ITestOutputHelper log, IDistributedCache tail) : IDistributedCache - { - protected ITestOutputHelper Log => log; - protected IDistributedCache Tail => tail; - - protected int opcount; - public int OpCount => Volatile.Read(ref opcount); - - byte[]? IDistributedCache.Get(string key) - { - Interlocked.Increment(ref opcount); - Log.WriteLine($"Get: {key}"); - return Tail.Get(key); - } - - Task IDistributedCache.GetAsync(string key, CancellationToken token) - { - Interlocked.Increment(ref opcount); - Log.WriteLine($"GetAsync: {key}"); - return Tail.GetAsync(key, token); - } - - void IDistributedCache.Refresh(string key) - { - Interlocked.Increment(ref opcount); - Log.WriteLine($"Refresh: {key}"); - Tail.Refresh(key); - } - - Task IDistributedCache.RefreshAsync(string key, CancellationToken token) - { - Interlocked.Increment(ref opcount); - Log.WriteLine($"RefreshAsync: {key}"); - return Tail.RefreshAsync(key, token); - } - - void IDistributedCache.Remove(string key) - { - Interlocked.Increment(ref opcount); - Log.WriteLine($"Remove: {key}"); - Tail.Remove(key); - } - - Task IDistributedCache.RemoveAsync(string key, CancellationToken token) - { - Interlocked.Increment(ref opcount); - Log.WriteLine($"RemoveAsync: {key}"); - return Tail.RemoveAsync(key, token); - } - - void IDistributedCache.Set(string key, byte[] value, DistributedCacheEntryOptions options) - { - Interlocked.Increment(ref opcount); - Log.WriteLine($"Set (byte[]): {key}"); - Tail.Set(key, value, options); - } - - Task IDistributedCache.SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token) - { - Interlocked.Increment(ref opcount); - Log.WriteLine($"SetAsync (byte[]): {key}"); - return Tail.SetAsync(key, value, options, token); - } - } - - private static string Me([CallerMemberName] string caller = "") => caller; -} diff --git a/src/Caching/Hybrid/test/Microsoft.Extensions.Caching.Hybrid.Tests.csproj b/src/Caching/Hybrid/test/Microsoft.Extensions.Caching.Hybrid.Tests.csproj deleted file mode 100644 index 4b862961ffa3..000000000000 --- a/src/Caching/Hybrid/test/Microsoft.Extensions.Caching.Hybrid.Tests.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - $(DefaultNetCoreTargetFramework);$(DefaultNetFxTargetFramework) - enable - enable - - - - - - - - - - - - - PreserveNewest - - - - diff --git a/src/Caching/Hybrid/test/RedisTests.cs b/src/Caching/Hybrid/test/RedisTests.cs deleted file mode 100644 index 32d3ca797a23..000000000000 --- a/src/Caching/Hybrid/test/RedisTests.cs +++ /dev/null @@ -1,103 +0,0 @@ -// 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.Internal; -using Microsoft.Extensions.Caching.StackExchangeRedis; -using Microsoft.Extensions.DependencyInjection; -using StackExchange.Redis; -using Xunit.Abstractions; - -namespace Microsoft.Extensions.Caching.Hybrid.Tests; - -public sealed class RedisFixture : IDisposable -{ - private ConnectionMultiplexer? _muxer; - private Task? _sharedConnect; - public Task ConnectAsync() => _sharedConnect ??= DoConnectAsync(); - - public void Dispose() => _muxer?.Dispose(); - - async Task DoConnectAsync() - { - try - { - _muxer = await ConnectionMultiplexer.ConnectAsync("127.0.0.1:6379"); - await _muxer.GetDatabase().PingAsync(); - return _muxer; - } - catch - { - return null; - } - } -} -public class RedisTests : DistributedCacheTests, IClassFixture -{ - private readonly RedisFixture _fixture; - public RedisTests(RedisFixture fixture, ITestOutputHelper log) : base(log) => _fixture = fixture; - - protected override bool CustomClockSupported => false; - - protected override async ValueTask ConfigureAsync(IServiceCollection services) - { - var redis = await _fixture.ConnectAsync(); - if (redis is null) - { - Log.WriteLine("Redis is not available"); - return; // inconclusive - } - Log.WriteLine("Redis is available"); - services.AddSingleton(redis); - services.AddStackExchangeRedisCache(options => - { - options.ConnectionMultiplexerFactory = () => Task.FromResult(redis); - }); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task BasicUsage(bool useBuffers) - { - var services = new ServiceCollection(); - await ConfigureAsync(services); - services.AddHybridCache(); - var provider = services.BuildServiceProvider(); // not "using" - that will tear down our redis; use the fixture for that - - var cache = Assert.IsType(provider.GetRequiredService()); - if (cache.BackendCache is null) - { - Log.WriteLine("Backend cache not available; inconclusive"); - return; - } - Assert.IsAssignableFrom(cache.BackendCache); - - if (!useBuffers) // force byte[] mode - { - cache.DebugRemoveFeatures(DefaultHybridCache.CacheFeatures.BackendBuffers); - } - Log.WriteLine($"features: {cache.GetFeatures()}"); - - var key = Me(); - var redis = provider.GetRequiredService(); - await redis.GetDatabase().KeyDeleteAsync(key); // start from known state - Assert.False(await redis.GetDatabase().KeyExistsAsync(key)); - - var count = 0; - for (var i = 0; i < 10; i++) - { - await cache.GetOrCreateAsync(key, _ => - { - Interlocked.Increment(ref count); - return new(Guid.NewGuid()); - }); - } - Assert.Equal(1, count); - - await Task.Delay(500); // the L2 write continues in the background; give it a chance - - var ttl = await redis.GetDatabase().KeyTimeToLiveAsync(key); - Log.WriteLine($"ttl: {ttl}"); - Assert.NotNull(ttl); - } -} diff --git a/src/Caching/Hybrid/test/SampleUsage.cs b/src/Caching/Hybrid/test/SampleUsage.cs deleted file mode 100644 index 78786a8b8877..000000000000 --- a/src/Caching/Hybrid/test/SampleUsage.cs +++ /dev/null @@ -1,195 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel; -using System.Text.Json; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Extensions.Caching.Hybrid.Tests; - -public class SampleUsage -{ - [Fact] - public async Task DistributedCacheWorks() - { - var services = new ServiceCollection(); - services.AddDistributedMemoryCache(); - services.AddTransient(); - using var provider = services.BuildServiceProvider(); - - var obj = provider.GetRequiredService(); - string name = "abc"; - int id = 42; - var x = await obj.GetSomeInformationAsync(name, id); - var y = await obj.GetSomeInformationAsync(name, id); - Assert.NotSame(x, y); - Assert.Equal(id, x.Id); - Assert.Equal(name, x.Name); - Assert.Equal(id, y.Id); - Assert.Equal(name, y.Name); - } - - [Fact] - public async Task HybridCacheWorks() - { - var services = new ServiceCollection(); - services.AddHybridCache(); - services.AddTransient(); - using var provider = services.BuildServiceProvider(); - - var obj = provider.GetRequiredService(); - string name = "abc"; - int id = 42; - var x = await obj.GetSomeInformationAsync(name, id); - var y = await obj.GetSomeInformationAsync(name, id); - Assert.NotSame(x, y); - Assert.Equal(id, x.Id); - Assert.Equal(name, x.Name); - Assert.Equal(id, y.Id); - Assert.Equal(name, y.Name); - } - - [Fact] - public async Task HybridCacheNoCaptureWorks() - { - var services = new ServiceCollection(); - services.AddHybridCache(); - services.AddTransient(); - using var provider = services.BuildServiceProvider(); - - var obj = provider.GetRequiredService(); - string name = "abc"; - int id = 42; - var x = await obj.GetSomeInformationAsync(name, id); - var y = await obj.GetSomeInformationAsync(name, id); - Assert.NotSame(x, y); - Assert.Equal(id, x.Id); - Assert.Equal(name, x.Name); - Assert.Equal(id, y.Id); - Assert.Equal(name, y.Name); - } - - [Fact] - public async Task HybridCacheNoCaptureObjReuseWorks() - { - var services = new ServiceCollection(); - services.AddHybridCache(); - services.AddTransient(); - using var provider = services.BuildServiceProvider(); - - var obj = provider.GetRequiredService(); - string name = "abc"; - int id = 42; - var x = await obj.GetSomeInformationAsync(name, id); - var y = await obj.GetSomeInformationAsync(name, id); - Assert.Same(x, y); - Assert.Equal(id, x.Id); - Assert.Equal(name, x.Name); - } - - public class SomeDCService(IDistributedCache cache) - { - public async Task GetSomeInformationAsync(string name, int id, CancellationToken token = default) - { - var key = $"someinfo:{name}:{id}"; // unique key for this combination - - var bytes = await cache.GetAsync(key, token); // try to get from cache - SomeInformation info; - if (bytes is null) - { - // cache miss; get the data from the real source - info = await SomeExpensiveOperationAsync(name, id, token); - - // serialize and cache it - bytes = SomeSerializer.Serialize(info); - await cache.SetAsync(key, bytes, token); - } - else - { - // cache hit; deserialize it - info = SomeSerializer.Deserialize(bytes); - } - return info; - } - } - - public class SomeHCService(HybridCache cache) - { - public async Task GetSomeInformationAsync(string name, int id, CancellationToken token = default) - { - return await cache.GetOrCreateAsync( - $"someinfo:{name}:{id}", // unique key for this combination - async ct => await SomeExpensiveOperationAsync(name, id, ct), - cancellationToken: token - ); - } - } - - // this is the work we're trying to cache - private static Task SomeExpensiveOperationAsync(string name, int id, - CancellationToken token = default) - { - return Task.FromResult(new SomeInformation { Id = id, Name = name }); - } - private static Task SomeExpensiveOperationReuseAsync(string name, int id, - CancellationToken token = default) - { - return Task.FromResult(new SomeInformationReuse { Id = id, Name = name }); - } - - public class SomeHCServiceNoCapture(HybridCache cache) - { - public async Task GetSomeInformationAsync(string name, int id, CancellationToken token = default) - { - return await cache.GetOrCreateAsync( - $"someinfo:{name}:{id}", // unique key for this combination - (name, id), // all of the state we need for the final call, if needed - static async (state, token) => - await SomeExpensiveOperationAsync(state.name, state.id, token), - cancellationToken: token - ); - } - } - - public class SomeHCServiceNoCaptureObjReuse(HybridCache cache, CancellationToken token = default) - { - public async Task GetSomeInformationAsync(string name, int id) - { - return await cache.GetOrCreateAsync( - $"someinfo:{name}:{id}", // unique key for this combination - (name, id), // all of the state we need for the final call, if needed - static async (state, token) => - await SomeExpensiveOperationReuseAsync(state.name, state.id, token), - cancellationToken: token - ); - } - } - - static class SomeSerializer - { - internal static T Deserialize(byte[] bytes) - { - return JsonSerializer.Deserialize(bytes)!; - } - - internal static byte[] Serialize(T info) - { - using var ms = new MemoryStream(); - JsonSerializer.Serialize(ms, info); - return ms.ToArray(); - } - } - public class SomeInformation - { - public int Id { get; set; } - public string? Name { get; set; } - } - - [ImmutableObject(true)] - public sealed class SomeInformationReuse - { - public int Id { get; set; } - public string? Name { get; set; } - } -} diff --git a/src/Caching/Hybrid/test/ServiceConstructionTests.cs b/src/Caching/Hybrid/test/ServiceConstructionTests.cs deleted file mode 100644 index 103ac87ed890..000000000000 --- a/src/Caching/Hybrid/test/ServiceConstructionTests.cs +++ /dev/null @@ -1,234 +0,0 @@ -// 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.Runtime.CompilerServices; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Hybrid.Internal; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Configuration.Json; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously -#pragma warning disable CS8769 // Nullability of reference types in type of parameter doesn't match implemented member (possibly because of nullability attributes). - -namespace Microsoft.Extensions.Caching.Hybrid.Tests; -public class ServiceConstructionTests -{ - [Fact] - public void CanCreateDefaultService() - { - var services = new ServiceCollection(); - services.AddHybridCache(); - using var provider = services.BuildServiceProvider(); - Assert.IsType(provider.GetService()); - } - - [Fact] - public void CanCreateServiceWithManualOptions() - { - var services = new ServiceCollection(); - services.AddHybridCache(options => - { - options.MaximumKeyLength = 937; - options.DefaultEntryOptions = new() { Expiration = TimeSpan.FromSeconds(120), Flags = HybridCacheEntryFlags.DisableLocalCacheRead }; - }); - using var provider = services.BuildServiceProvider(); - var obj = Assert.IsType(provider.GetService()); - var options = obj.Options; - Assert.Equal(937, options.MaximumKeyLength); - var defaults = options.DefaultEntryOptions; - Assert.NotNull(defaults); - Assert.Equal(TimeSpan.FromSeconds(120), defaults.Expiration); - Assert.Equal(HybridCacheEntryFlags.DisableLocalCacheRead, defaults.Flags); - Assert.Null(defaults.LocalCacheExpiration); // wasn't specified - } - - [Fact] - public void CanParseOptions_NoEntryOptions() - { - var source = new JsonConfigurationSource { Path = "BasicConfig.json" }; - var configBuilder = new ConfigurationBuilder { Sources = { source } }; - var config = configBuilder.Build(); - var options = new HybridCacheOptions(); - ConfigurationBinder.Bind(config, "no_entry_options", options); - - Assert.Equal(937, options.MaximumKeyLength); - Assert.Null(options.DefaultEntryOptions); - } - [Fact] - public void CanParseOptions_WithEntryOptions() // in particular, check we can parse the timespan and [Flags] enums - { - var source = new JsonConfigurationSource { Path = "BasicConfig.json" }; - var configBuilder = new ConfigurationBuilder { Sources = { source } }; - var config = configBuilder.Build(); - var options = new HybridCacheOptions(); - ConfigurationBinder.Bind(config, "with_entry_options", options); - - Assert.Equal(937, options.MaximumKeyLength); - var defaults = options.DefaultEntryOptions; - Assert.NotNull(defaults); - Assert.Equal(HybridCacheEntryFlags.DisableCompression | HybridCacheEntryFlags.DisableLocalCacheRead, defaults.Flags); - Assert.Equal(TimeSpan.FromSeconds(120), defaults.LocalCacheExpiration); - Assert.Null(defaults.Expiration); // wasn't specified - } - - [Fact] - public async Task BasicStatelessUsage() - { - var services = new ServiceCollection(); - services.AddHybridCache(); - using var provider = services.BuildServiceProvider(); - var cache = provider.GetRequiredService(); - - var expected = Guid.NewGuid().ToString(); - var actual = await cache.GetOrCreateAsync(Me(), async _ => expected); - Assert.Equal(expected, actual); - } - - [Fact] - public async Task BasicStatefulUsage() - { - var services = new ServiceCollection(); - services.AddHybridCache(); - using var provider = services.BuildServiceProvider(); - var cache = provider.GetRequiredService(); - - var expected = Guid.NewGuid().ToString(); - var actual = await cache.GetOrCreateAsync(Me(), expected, async (state, _) => state); - Assert.Equal(expected, actual); - } - - [Fact] - public void DefaultSerializerConfiguration() - { - var services = new ServiceCollection(); - services.AddHybridCache(); - using var provider = services.BuildServiceProvider(); - var cache = Assert.IsType(provider.GetRequiredService()); - - Assert.IsType(cache.GetSerializer()); - Assert.IsType(cache.GetSerializer()); - Assert.IsType>(cache.GetSerializer()); - Assert.IsType>(cache.GetSerializer()); - } - - [Fact] - public void CustomSerializerConfiguration() - { - var services = new ServiceCollection(); - services.AddHybridCache().AddSerializer(); - using var provider = services.BuildServiceProvider(); - var cache = Assert.IsType(provider.GetRequiredService()); - - Assert.IsType(cache.GetSerializer()); - Assert.IsType>(cache.GetSerializer()); - } - - [Fact] - public void CustomSerializerFactoryConfiguration() - { - var services = new ServiceCollection(); - services.AddHybridCache().AddSerializerFactory(); - using var provider = services.BuildServiceProvider(); - var cache = Assert.IsType(provider.GetRequiredService()); - - Assert.IsType(cache.GetSerializer()); - Assert.IsType>(cache.GetSerializer()); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void DefaultMemoryDistributedCacheIsIgnored(bool manual) - { - var services = new ServiceCollection(); - if (manual) - { - services.AddSingleton(); - } - else - { - services.AddDistributedMemoryCache(); - } - services.AddHybridCache(); - using var provider = services.BuildServiceProvider(); - var cache = Assert.IsType(provider.GetRequiredService()); - - Assert.Null(cache.BackendCache); - } - - [Fact] - public void SubclassMemoryDistributedCacheIsNotIgnored() - { - var services = new ServiceCollection(); - services.AddSingleton(); - services.AddHybridCache(); - using var provider = services.BuildServiceProvider(); - var cache = Assert.IsType(provider.GetRequiredService()); - - Assert.NotNull(cache.BackendCache); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void SubclassMemoryCacheIsNotIgnored(bool manual) - { - var services = new ServiceCollection(); - if (manual) - { - services.AddSingleton(); - } - else - { - services.AddDistributedMemoryCache(); - } - services.AddSingleton(); - services.AddHybridCache(); - using var provider = services.BuildServiceProvider(); - var cache = Assert.IsType(provider.GetRequiredService()); - - Assert.NotNull(cache.BackendCache); - } - - class CustomMemoryCache : MemoryCache - { - public CustomMemoryCache(IOptions options) : base(options) { } - public CustomMemoryCache(IOptions options, ILoggerFactory loggerFactory) : base(options, loggerFactory) { } - } - - class CustomMemoryDistributedCache : MemoryDistributedCache - { - public CustomMemoryDistributedCache(IOptions options) : base(options) { } - public CustomMemoryDistributedCache(IOptions options, ILoggerFactory loggerFactory) : base(options, loggerFactory) { } - } - - class Customer { } - class Order { } - - class CustomerSerializer : IHybridCacheSerializer - { - Customer IHybridCacheSerializer.Deserialize(ReadOnlySequence source) => throw new NotImplementedException(); - void IHybridCacheSerializer.Serialize(Customer value, IBufferWriter target) => throw new NotImplementedException(); - } - - class CustomFactory : IHybridCacheSerializerFactory - { - bool IHybridCacheSerializerFactory.TryCreateSerializer(out IHybridCacheSerializer? serializer) - { - if (typeof(T) == typeof(Customer)) - { - serializer = (IHybridCacheSerializer)new CustomerSerializer(); - return true; - } - serializer = null; - return false; - } - } - - private static string Me([CallerMemberName] string caller = "") => caller; -} diff --git a/src/Caching/Hybrid/test/SqlServerTests.cs b/src/Caching/Hybrid/test/SqlServerTests.cs deleted file mode 100644 index bbfc18338933..000000000000 --- a/src/Caching/Hybrid/test/SqlServerTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Data.SqlClient; -using Microsoft.Extensions.DependencyInjection; -using Xunit.Abstractions; - -namespace Microsoft.Extensions.Caching.Hybrid.Tests; - -public class SqlServerTests : DistributedCacheTests -{ - public SqlServerTests(ITestOutputHelper log) : base(log) { } - - protected override bool CustomClockSupported => true; - - protected override async ValueTask ConfigureAsync(IServiceCollection services) - { - // create a local DB named CacheBench, then - // dotnet tool install --global dotnet-sql-cache - // dotnet sql-cache create "Data Source=.;Initial Catalog=CacheBench;Integrated Security=True;Trust Server Certificate=True" dbo BenchmarkCache - - const string ConnectionString = "Data Source=.;Initial Catalog=CacheBench;Integrated Security=True;Trust Server Certificate=True"; - - try - { - using var conn = new SqlConnection(ConnectionString); - using var cmd = conn.CreateCommand(); - cmd.CommandText = "truncate table dbo.BenchmarkCache"; - await conn.OpenAsync(); - await cmd.ExecuteNonQueryAsync(); - - // if that worked: we should be fine - services.AddDistributedSqlServerCache(options => - { - options.SchemaName = "dbo"; - options.TableName = "BenchmarkCache"; - options.ConnectionString = ConnectionString; - options.SystemClock = Clock; - }); - } - catch (Exception ex) - { - Log.WriteLine(ex.Message); - } - } -} diff --git a/src/Caching/Hybrid/test/StampedeTests.cs b/src/Caching/Hybrid/test/StampedeTests.cs deleted file mode 100644 index bfd777954566..000000000000 --- a/src/Caching/Hybrid/test/StampedeTests.cs +++ /dev/null @@ -1,414 +0,0 @@ -// 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.Generic; -using System.ComponentModel; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.InternalTesting; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Hybrid.Internal; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace Microsoft.Extensions.Caching.Hybrid.Tests; -public class StampedeTests -{ - static ServiceProvider GetDefaultCache(out DefaultHybridCache cache) - { - var services = new ServiceCollection(); - services.AddSingleton(); - services.AddSingleton(); - services.AddHybridCache(options => - { - options.DefaultEntryOptions = new() - { - Flags = HybridCacheEntryFlags.DisableDistributedCache | HybridCacheEntryFlags.DisableLocalCache - }; - }); - var provider = services.BuildServiceProvider(); - cache = Assert.IsType(provider.GetRequiredService()); - return provider; - } - - public sealed class InvalidCache : IDistributedCache, IMemoryCache - { - void IDisposable.Dispose() { } - ICacheEntry IMemoryCache.CreateEntry(object key) => throw new NotSupportedException("Intentionally not provided"); - - byte[]? IDistributedCache.Get(string key) => throw new NotSupportedException("Intentionally not provided"); - - Task IDistributedCache.GetAsync(string key, CancellationToken token) => throw new NotSupportedException("Intentionally not provided"); - - void IDistributedCache.Refresh(string key) => throw new NotSupportedException("Intentionally not provided"); - - Task IDistributedCache.RefreshAsync(string key, CancellationToken token) => throw new NotSupportedException("Intentionally not provided"); - - void IDistributedCache.Remove(string key) => throw new NotSupportedException("Intentionally not provided"); - - void IMemoryCache.Remove(object key) => throw new NotSupportedException("Intentionally not provided"); - - Task IDistributedCache.RemoveAsync(string key, CancellationToken token) => throw new NotSupportedException("Intentionally not provided"); - - void IDistributedCache.Set(string key, byte[] value, DistributedCacheEntryOptions options) => throw new NotSupportedException("Intentionally not provided"); - - Task IDistributedCache.SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token) => throw new NotSupportedException("Intentionally not provided"); - - bool IMemoryCache.TryGetValue(object key, out object? value) => throw new NotSupportedException("Intentionally not provided"); - } - - [Theory] - [InlineData(1, false)] - [InlineData(1, true)] - [InlineData(10, false)] - [InlineData(10, true)] - public async Task MultipleCallsShareExecution_NoCancellation(int callerCount, bool canBeCanceled) - { - using var scope = GetDefaultCache(out var cache); - using var semaphore = new SemaphoreSlim(0); - - var token = canBeCanceled ? new CancellationTokenSource().Token : CancellationToken.None; - - int executeCount = 0, cancelCount = 0; - var results = new Task[callerCount]; - for (var i = 0; i < callerCount; i++) - { - results[i] = cache.GetOrCreateAsync(Me(), async ct => - { - using var reg = ct.Register(() => Interlocked.Increment(ref cancelCount)); - if (!await semaphore.WaitAsync(5_000, CancellationToken.None)) - { - throw new TimeoutException("Failed to activate"); - } - Interlocked.Increment(ref executeCount); - ct.ThrowIfCancellationRequested(); // assert not cancelled - return Guid.NewGuid(); - }, cancellationToken: token).AsTask(); - } - - Assert.Equal(callerCount, cache.DebugGetCallerCount(Me())); - - // everyone is queued up; release the hounds and check - // that we all got the same result - Assert.Equal(0, Volatile.Read(ref executeCount)); - Assert.Equal(0, Volatile.Read(ref cancelCount)); - semaphore.Release(); - var first = await results[0]; - Assert.Equal(1, Volatile.Read(ref executeCount)); - Assert.Equal(0, Volatile.Read(ref cancelCount)); - foreach (var result in results) - { - Assert.Equal(first, await result); - } - Assert.Equal(1, Volatile.Read(ref executeCount)); - Assert.Equal(0, Volatile.Read(ref cancelCount)); - - // and do it a second time; we expect different results - Volatile.Write(ref executeCount, 0); - for (var i = 0; i < callerCount; i++) - { - results[i] = cache.GetOrCreateAsync(Me(), async ct => - { - using var reg = ct.Register(() => Interlocked.Increment(ref cancelCount)); - if (!await semaphore.WaitAsync(5_000, CancellationToken.None)) - { - throw new TimeoutException("Failed to activate"); - } - Interlocked.Increment(ref executeCount); - ct.ThrowIfCancellationRequested(); // assert not cancelled - return Guid.NewGuid(); - }, cancellationToken: token).AsTask(); - } - - Assert.Equal(callerCount, cache.DebugGetCallerCount(Me())); - - // everyone is queued up; release the hounds and check - // that we all got the same result - Assert.Equal(0, Volatile.Read(ref executeCount)); - Assert.Equal(0, Volatile.Read(ref cancelCount)); - semaphore.Release(); - var second = await results[0]; - Assert.NotEqual(first, second); - Assert.Equal(1, Volatile.Read(ref executeCount)); - Assert.Equal(0, Volatile.Read(ref cancelCount)); - foreach (var result in results) - { - Assert.Equal(second, await result); - } - Assert.Equal(1, Volatile.Read(ref executeCount)); - Assert.Equal(0, Volatile.Read(ref cancelCount)); - } - - [Theory] - [InlineData(1)] - [InlineData(10)] - public async Task MultipleCallsShareExecution_EveryoneCancels(int callerCount) - { - // what we want to prove here is that everyone ends up cancelling promptly by - // *their own* cancellation (not dependent on the shared task), and that - // the shared task becomes cancelled (which can be later) - - using var scope = GetDefaultCache(out var cache); - using var semaphore = new SemaphoreSlim(0); - - int executeCount = 0, cancelCount = 0; - var results = new Task[callerCount]; - var cancels = new CancellationTokenSource[callerCount]; - for (var i = 0; i < callerCount; i++) - { - cancels[i] = new CancellationTokenSource(); - results[i] = cache.GetOrCreateAsync(Me(), async ct => - { - using var reg = ct.Register(() => Interlocked.Increment(ref cancelCount)); - if (!await semaphore.WaitAsync(5_000, CancellationToken.None)) - { - throw new TimeoutException("Failed to activate"); - } - try - { - Interlocked.Increment(ref executeCount); - ct.ThrowIfCancellationRequested(); - return Guid.NewGuid(); - } - finally - { - semaphore.Release(); // handshake so we can check when available again - } - }, cancellationToken: cancels[i].Token).AsTask(); - } - - Assert.Equal(callerCount, cache.DebugGetCallerCount(Me())); - - // everyone is queued up; release the hounds and check - // that we all got the same result - foreach (var cancel in cancels) - { - cancel.Cancel(); - } - await Task.Delay(500); // cancellation happens on a worker; need to allow a moment - for (var i = 0; i < callerCount; i++) - { - var result = results[i]; - // should have already cancelled, even though underlying task hasn't finished yet - Assert.Equal(TaskStatus.Canceled, result.Status); - var ex = Assert.Throws(() => result.GetAwaiter().GetResult()); - Assert.Equal(cancels[i].Token, ex.CancellationToken); // each gets the correct blame - } - - Assert.Equal(0, Volatile.Read(ref executeCount)); - semaphore.Release(); - if (!await semaphore.WaitAsync(5_000)) // wait for underlying task to hand back to us - { - throw new TimeoutException("Didn't get handshake back from task"); - } - Assert.Equal(1, Volatile.Read(ref executeCount)); - Assert.Equal(1, Volatile.Read(ref cancelCount)); - } - - [Theory] - [InlineData(2, 0)] - [InlineData(2, 1)] - [InlineData(10, 0)] - [InlineData(10, 1)] - [InlineData(10, 7)] - [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/55474")] - public async Task MultipleCallsShareExecution_MostCancel(int callerCount, int remaining) - { - Assert.True(callerCount >= 2); // "most" is not "one" - - // what we want to prove here is that everyone ends up cancelling promptly by - // *their own* cancellation (not dependent on the shared task), and that - // the shared task becomes cancelled (which can be later) - - using var scope = GetDefaultCache(out var cache); - using var semaphore = new SemaphoreSlim(0); - - int executeCount = 0, cancelCount = 0; - var results = new Task[callerCount]; - var cancels = new CancellationTokenSource[callerCount]; - for (var i = 0; i < callerCount; i++) - { - cancels[i] = new CancellationTokenSource(); - results[i] = cache.GetOrCreateAsync(Me(), async ct => - { - using var reg = ct.Register(() => Interlocked.Increment(ref cancelCount)); - if (!await semaphore.WaitAsync(5_000, CancellationToken.None)) - { - throw new TimeoutException("Failed to activate"); - } - try - { - Interlocked.Increment(ref executeCount); - ct.ThrowIfCancellationRequested(); - return Guid.NewGuid(); - } - finally - { - semaphore.Release(); // handshake so we can check when available again - } - }, cancellationToken: cancels[i].Token).AsTask(); - } - - Assert.Equal(callerCount, cache.DebugGetCallerCount(Me())); - - // everyone is queued up; release the hounds and check - // that we all got the same result - for (var i = 0; i < callerCount; i++) - { - if (i != remaining) - { - cancels[i].Cancel(); - } - } - await Task.Delay(500); // cancellation happens on a worker; need to allow a moment - for (var i = 0; i < callerCount; i++) - { - if (i != remaining) - { - var result = results[i]; - // should have already cancelled, even though underlying task hasn't finished yet - Assert.Equal(TaskStatus.Canceled, result.Status); - var ex = Assert.Throws(() => result.GetAwaiter().GetResult()); - Assert.Equal(cancels[i].Token, ex.CancellationToken); // each gets the correct blame - } - } - - Assert.Equal(0, Volatile.Read(ref executeCount)); - semaphore.Release(); - if (!await semaphore.WaitAsync(5_000)) // wait for underlying task to hand back to us - { - throw new TimeoutException("Didn't get handshake back from task"); - } - Assert.Equal(1, Volatile.Read(ref executeCount)); - Assert.Equal(0, Volatile.Read(ref cancelCount)); // ran to completion - await results[remaining]; - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ImmutableTypesShareFinalTask(bool withCancelation) - { - var token = withCancelation ? new CancellationTokenSource().Token : CancellationToken.None; - - using var scope = GetDefaultCache(out var cache); - using var semaphore = new SemaphoreSlim(0); - - // note AsTask *in this scenario* fetches the underlying incomplete task - var first = cache.GetOrCreateAsync(Me(), async ct => { await semaphore.WaitAsync(CancellationToken.None); semaphore.Release(); return Guid.NewGuid(); }, cancellationToken: token).AsTask(); - var second = cache.GetOrCreateAsync(Me(), async ct => { await semaphore.WaitAsync(CancellationToken.None); semaphore.Release(); return Guid.NewGuid(); }, cancellationToken: token).AsTask(); - - if (withCancelation) - { - Assert.NotSame(first, second); - } - else - { - Assert.Same(first, second); - } - semaphore.Release(); - Assert.Equal(await first, await second); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ImmutableCustomTypesShareFinalTask(bool withCancelation) - { - var token = withCancelation ? new CancellationTokenSource().Token : CancellationToken.None; - - using var scope = GetDefaultCache(out var cache); - using var semaphore = new SemaphoreSlim(0); - - // AsTask *in this scenario* fetches the underlying incomplete task - var first = cache.GetOrCreateAsync(Me(), async ct => { await semaphore.WaitAsync(CancellationToken.None); semaphore.Release(); return new Immutable(Guid.NewGuid()); }, cancellationToken: token).AsTask(); - var second = cache.GetOrCreateAsync(Me(), async ct => { await semaphore.WaitAsync(CancellationToken.None); semaphore.Release(); return new Immutable(Guid.NewGuid()); }, cancellationToken: token).AsTask(); - - if (withCancelation) - { - Assert.NotSame(first, second); - } - else - { - Assert.Same(first, second); - } - semaphore.Release(); - - var x = await first; - var y = await second; - Assert.Equal(x.Value, y.Value); - Assert.Same(x, y); // same instance regardless of whether the tasks were shared - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task MutableTypesNeverShareFinalTask(bool withCancelation) - { - var token = withCancelation ? new CancellationTokenSource().Token : CancellationToken.None; - - using var scope = GetDefaultCache(out var cache); - using var semaphore = new SemaphoreSlim(0); - - // AsTask *in this scenario* fetches the underlying incomplete task - var first = cache.GetOrCreateAsync(Me(), async ct => { await semaphore.WaitAsync(CancellationToken.None); semaphore.Release(); return new Mutable(Guid.NewGuid()); }, cancellationToken: token).AsTask(); - var second = cache.GetOrCreateAsync(Me(), async ct => { await semaphore.WaitAsync(CancellationToken.None); semaphore.Release(); return new Mutable(Guid.NewGuid()); }, cancellationToken: token).AsTask(); - - Assert.NotSame(first, second); - semaphore.Release(); - - var x = await first; - var y = await second; - Assert.Equal(x.Value, y.Value); - Assert.NotSame(x, y); - } - - [Fact] - public void ValidatePartitioning() - { - // we just want to validate that key-level partitioning is - // happening to some degree, i.e. it isn't fundamentally broken - using var scope = GetDefaultCache(out var cache); - Dictionary counts = []; - for(int i = 0; i < 1024; i++) - { - var key = new DefaultHybridCache.StampedeKey(Guid.NewGuid().ToString(), default); - var obj = cache.GetPartitionedSyncLock(in key); - if (!counts.TryGetValue(obj, out var count)) - { - count = 0; - } - counts[obj] = count + 1; - } - - // We just want to prove that we got 8 non-empty partitions. - // This is *technically* non-deterministic, but: we'd - // need to be having a very bad day for the math gods - // to conspire against us that badly - if this test - // starts failing, maybe buy a lottery ticket? - Assert.Equal(8, counts.Count); - foreach (var pair in counts) - { - // the *median* should be 128 here; let's - // not be aggressive about it, though - Assert.True(pair.Value > 16); - } - } - - class Mutable(Guid value) - { - public Guid Value => value; - } - - [ImmutableObject(true)] - public sealed class Immutable(Guid value) - { - public Guid Value => value; - } - - private static string Me([CallerMemberName] string caller = "") => caller; -} diff --git a/src/Caching/Hybrid/test/TypeTests.cs b/src/Caching/Hybrid/test/TypeTests.cs deleted file mode 100644 index dc1f6f06749b..000000000000 --- a/src/Caching/Hybrid/test/TypeTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -// 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.Generic; -using System.ComponentModel; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Hybrid.Internal; - -namespace Microsoft.Extensions.Caching.Hybrid.Tests; -public class TypeTests -{ - [Theory] - [InlineData(typeof(string))] - [InlineData(typeof(int))] // primitive - [InlineData(typeof(int?))] - [InlineData(typeof(Guid))] // non-primitive but blittable - [InlineData(typeof(Guid?))] - [InlineData(typeof(SealedCustomClassAttribTrue))] // attrib says explicitly true, and sealed - [InlineData(typeof(CustomBlittableStruct))] // blittable, and we're copying each time - [InlineData(typeof(CustomNonBlittableStructAttribTrue))] // non-blittable, attrib says explicitly true - public void ImmutableTypes(Type type) - { - Assert.True((bool)typeof(DefaultHybridCache.ImmutableTypeCache<>).MakeGenericType(type) - .GetField(nameof(DefaultHybridCache.ImmutableTypeCache.IsImmutable), BindingFlags.Static | BindingFlags.Public)! - .GetValue(null)!); - } - - [Theory] - [InlineData(typeof(byte[]))] - [InlineData(typeof(string[]))] - [InlineData(typeof(object))] - [InlineData(typeof(CustomClassNoAttrib))] // no attrib, who knows? - [InlineData(typeof(CustomClassAttribFalse))] // attrib says explicitly no - [InlineData(typeof(CustomClassAttribTrue))] // attrib says explicitly true, but not sealed: we might have a sub-class - [InlineData(typeof(CustomNonBlittableStructNoAttrib))] // no attrib, who knows? - [InlineData(typeof(CustomNonBlittableStructAttribFalse))] // attrib says explicitly no - public void MutableTypes(Type type) - { - Assert.False((bool)typeof(DefaultHybridCache.ImmutableTypeCache<>).MakeGenericType(type) - .GetField(nameof(DefaultHybridCache.ImmutableTypeCache.IsImmutable), BindingFlags.Static | BindingFlags.Public)! - .GetValue(null)!); - } - - class CustomClassNoAttrib { } - [ImmutableObject(false)] - class CustomClassAttribFalse { } - [ImmutableObject(true)] - class CustomClassAttribTrue { } - [ImmutableObject(true)] - sealed class SealedCustomClassAttribTrue { } - - struct CustomBlittableStruct(int x) { public int X => x; } - struct CustomNonBlittableStructNoAttrib(string x) { public string X => x; } - [ImmutableObject(false)] - struct CustomNonBlittableStructAttribFalse(string x) { public string X => x; } - [ImmutableObject(true)] - struct CustomNonBlittableStructAttribTrue(string x) { public string X => x; } -} diff --git a/src/Caching/perf/MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks/DistributedCacheBenchmarks.cs b/src/Caching/perf/MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks/DistributedCacheBenchmarks.cs index 132a9cb6c3a8..c94d8ec8a8f7 100644 --- a/src/Caching/perf/MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks/DistributedCacheBenchmarks.cs +++ b/src/Caching/perf/MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks/DistributedCacheBenchmarks.cs @@ -7,7 +7,6 @@ using BenchmarkDotNet.Attributes; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Hybrid.Internal; using Microsoft.Extensions.DependencyInjection; using StackExchange.Redis; @@ -125,23 +124,6 @@ public int GetSingleRandom() return total; } - [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] - public int GetSingleRandomBuffer() - { - var writer = RecyclableArrayBufferWriter.Create(int.MaxValue); - int total = 0; - for (int i = 0; i < OperationsPerInvoke; i++) - { - if (_backend.TryGet(RandomKey(), writer)) - { - total += writer.CommittedBytes; - } - writer.ResetInPlace(); - } - writer.Dispose(); - return total; - } - [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] public int GetConcurrentRandom() { @@ -170,23 +152,6 @@ public async Task GetSingleRandomAsync() return total; } - [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] - public async Task GetSingleRandomBufferAsync() - { - var writer = RecyclableArrayBufferWriter.Create(int.MaxValue); - int total = 0; - for (int i = 0; i < OperationsPerInvoke; i++) - { - if (await _backend.TryGetAsync(RandomKey(), writer)) - { - total += writer.CommittedBytes; - } - writer.ResetInPlace(); - } - writer.Dispose(); - return total; - } - [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] public async Task GetConcurrentRandomAsync() { @@ -214,23 +179,6 @@ public int GetSingleFixed() return total; } - [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] - public int GetSingleFixedBuffer() - { - var writer = RecyclableArrayBufferWriter.Create(int.MaxValue); - int total = 0; - for (int i = 0; i < OperationsPerInvoke; i++) - { - if (_backend.TryGet(FixedKey(), writer)) - { - total += writer.CommittedBytes; - } - writer.ResetInPlace(); - } - writer.Dispose(); - return total; - } - [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] public int GetConcurrentFixed() { @@ -258,23 +206,6 @@ public async Task GetSingleFixedAsync() return total; } - [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] - public async Task GetSingleFixedBufferAsync() - { - var writer = RecyclableArrayBufferWriter.Create(int.MaxValue); - int total = 0; - for (int i = 0; i < OperationsPerInvoke; i++) - { - if (await _backend.TryGetAsync(FixedKey(), writer)) - { - total += writer.CommittedBytes; - } - writer.ResetInPlace(); - } - writer.Dispose(); - return total; - } - [Benchmark(OperationsPerInvoke = OperationsPerInvoke)] public async Task GetConcurrentFixedAsync() { diff --git a/src/Caching/perf/MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks/HybridCacheBenchmarks.cs b/src/Caching/perf/MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks/HybridCacheBenchmarks.cs deleted file mode 100644 index 3f68c9468040..000000000000 --- a/src/Caching/perf/MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks/HybridCacheBenchmarks.cs +++ /dev/null @@ -1,146 +0,0 @@ -// 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.ComponentModel; -using System.IO; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using BenchmarkDotNet.Attributes; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Hybrid; -using Microsoft.Extensions.DependencyInjection; -using StackExchange.Redis; - -namespace Microsoft.Extensions.Caching.Benchmarks; - -[MemoryDiagnoser] -public class HybridCacheBenchmarks : IDisposable -{ - private const string RedisConfigurationString = "127.0.0.1,AllowAdmin=true"; - private readonly ConnectionMultiplexer _multiplexer; - private readonly IDistributedCache _distributed; - private readonly HybridCache _hybrid; - public HybridCacheBenchmarks() - { - _multiplexer = ConnectionMultiplexer.Connect(RedisConfigurationString); - var services = new ServiceCollection(); - services.AddStackExchangeRedisCache(options => - { - options.ConnectionMultiplexerFactory = () => Task.FromResult(_multiplexer); - }); - services.AddHybridCache(); - var provider = services.BuildServiceProvider(); - - _distributed = provider.GetRequiredService(); - - _distributed.Remove(KeyDirect); - _distributed.Remove(KeyHybrid); - _distributed.Remove(KeyHybridImmutable); - - _hybrid = provider.GetRequiredService(); - } - - private const string KeyDirect = "direct"; - private const string KeyHybrid = "hybrid"; - private const string KeyHybridImmutable = "I_brid"; // want 6 chars - - public void Dispose() => _multiplexer.Dispose(); - - private const int CustomerId = 42; - - private static readonly DistributedCacheEntryOptions OneHour = new DistributedCacheEntryOptions() - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) - }; - - // scenario: 100% (or as-near-as) cache hit rate - [Benchmark(Baseline = true)] - public async ValueTask HitDistributedCache() - { - var bytes = await _distributed.GetAsync(KeyDirect); - if (bytes is null) - { - var cust = await Customer.GetAsync(CustomerId); - await _distributed.SetAsync(KeyDirect, Serialize(cust), OneHour); - return cust; - } - else - { - return Deserialize(bytes)!; - } - } - - // scenario: 100% (or as-near-as) cache hit rate - [Benchmark] - public ValueTask HitCaptureHybridCache() - => _hybrid.GetOrCreateAsync(KeyHybrid, - ct => Customer.GetAsync(CustomerId, ct)); - - // scenario: 100% (or as-near-as) cache hit rate - [Benchmark] - public ValueTask HitHybridCache() - => _hybrid.GetOrCreateAsync(KeyHybrid, CustomerId, - static (id, ct) => Customer.GetAsync(id, ct)); - - [Benchmark] - public ValueTask HitHybridCacheImmutable() // scenario: 100% (or as-near-as) cache hit rate - => _hybrid.GetOrCreateAsync(KeyHybridImmutable, CustomerId, static (id, ct) => ImmutableCustomer.GetAsync(id, ct)); - - private static byte[] Serialize(T obj) - { - using var ms = new MemoryStream(); - JsonSerializer.Serialize(ms, obj); - return ms.ToArray(); - } - - private static T? Deserialize(byte[] bytes) - { - using var ms = new MemoryStream(); - return JsonSerializer.Deserialize(bytes); - } - - public class Customer - { - public static ValueTask GetAsync(int id, CancellationToken token = default) - => new(new Customer - { - Id = id, - Name = "Random customer", - Region = 2, - Description = "Good for testing", - CreationDate = new DateTime(2024, 04, 17), - OrderValue = 123_456.789M - }); - - public int Id { get; set; } - public string? Name {get; set; } - public int Region { get; set; } - public string? Description { get; set; } - public DateTime CreationDate { get; set; } - public decimal OrderValue { get; set; } - } - - [ImmutableObject(true)] - public sealed class ImmutableCustomer - { - public static ValueTask GetAsync(int id, CancellationToken token = default) - => new(new ImmutableCustomer - { - Id = id, - Name = "Random customer", - Region = 2, - Description = "Good for testing", - CreationDate = new DateTime(2024, 04, 17), - OrderValue = 123_456.789M - }); - - public int Id { get; init; } - public string? Name { get; init; } - public int Region { get; init; } - public string? Description { get; init; } - public DateTime CreationDate { get; init; } - public decimal OrderValue { get; init; } - } -} diff --git a/src/Caching/perf/MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks.csproj b/src/Caching/perf/MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks.csproj index f58d80e677d6..9f75b176aac9 100644 --- a/src/Caching/perf/MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks.csproj +++ b/src/Caching/perf/MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks.csproj @@ -12,13 +12,11 @@ - - diff --git a/src/Caching/perf/MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks/Program.cs b/src/Caching/perf/MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks/Program.cs index bfb33dc46a30..8f33f9a22d1c 100644 --- a/src/Caching/perf/MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks/Program.cs +++ b/src/Caching/perf/MicroBenchmarks/Microsoft.Extensions.Caching.MicroBenchmarks/Program.cs @@ -6,17 +6,7 @@ using Microsoft.Extensions.Caching.Benchmarks; #if DEBUG -// validation -using (var hc = new HybridCacheBenchmarks()) -{ - for (int i = 0; i < 10; i++) - { - Console.WriteLine((await hc.HitDistributedCache()).Name); - Console.WriteLine((await hc.HitHybridCache()).Name); - Console.WriteLine((await hc.HitHybridCacheImmutable()).Name); - } -} - +Console.WriteLine("Release mode only"); /* using (var obj = new DistributedCacheBenchmarks { PayloadSize = 11512, Sliding = true }) {