Skip to content

Commit 8660e3a

Browse files
authored
Implement minimal implementation of HybridCache (#55147)
initial implementation of `HybridCache`
1 parent 4d11def commit 8660e3a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+4142
-119
lines changed

AspNetCore.sln

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,6 +1796,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Cachin
17961796
EndProject
17971797
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}"
17981798
EndProject
1799+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MicroBenchmarks", "MicroBenchmarks", "{6469F11E-8CEE-4292-820B-324DFFC88EBC}"
1800+
EndProject
1801+
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}"
17991802
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{9DC6B242-457B-4767-A84B-C3D23B76C642}"
18001803
EndProject
18011804
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi.Microbenchmarks", "src\OpenApi\perf\Microbenchmarks\Microsoft.AspNetCore.OpenApi.Microbenchmarks.csproj", "{D53F0EF7-0CDC-49B4-AA2D-229901B0A734}"
@@ -10849,6 +10852,22 @@ Global
1084910852
{CF63C942-895A-4F6B-888A-7653D7C4991A}.Release|x64.Build.0 = Release|Any CPU
1085010853
{CF63C942-895A-4F6B-888A-7653D7C4991A}.Release|x86.ActiveCfg = Release|Any CPU
1085110854
{CF63C942-895A-4F6B-888A-7653D7C4991A}.Release|x86.Build.0 = Release|Any CPU
10855+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
10856+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|Any CPU.Build.0 = Debug|Any CPU
10857+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|arm64.ActiveCfg = Debug|Any CPU
10858+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|arm64.Build.0 = Debug|Any CPU
10859+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|x64.ActiveCfg = Debug|Any CPU
10860+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|x64.Build.0 = Debug|Any CPU
10861+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|x86.ActiveCfg = Debug|Any CPU
10862+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|x86.Build.0 = Debug|Any CPU
10863+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|Any CPU.ActiveCfg = Release|Any CPU
10864+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|Any CPU.Build.0 = Release|Any CPU
10865+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|arm64.ActiveCfg = Release|Any CPU
10866+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|arm64.Build.0 = Release|Any CPU
10867+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|x64.ActiveCfg = Release|Any CPU
10868+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|x64.Build.0 = Release|Any CPU
10869+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|x86.ActiveCfg = Release|Any CPU
10870+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|x86.Build.0 = Release|Any CPU
1085210871
{D53F0EF7-0CDC-49B4-AA2D-229901B0A734}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1085310872
{D53F0EF7-0CDC-49B4-AA2D-229901B0A734}.Debug|Any CPU.Build.0 = Debug|Any CPU
1085410873
{D53F0EF7-0CDC-49B4-AA2D-229901B0A734}.Debug|arm64.ActiveCfg = Debug|Any CPU
@@ -11752,6 +11771,8 @@ Global
1175211771
{2D64CA23-6E81-488E-A7D3-9BDF87240098} = {0F39820F-F4A5-41C6-9809-D79B68F032EF}
1175311772
{2B60E6D3-9E7C-427A-AD4E-BBE9A6D935B9} = {2D64CA23-6E81-488E-A7D3-9BDF87240098}
1175411773
{CF63C942-895A-4F6B-888A-7653D7C4991A} = {2D64CA23-6E81-488E-A7D3-9BDF87240098}
11774+
{6469F11E-8CEE-4292-820B-324DFFC88EBC} = {0F39820F-F4A5-41C6-9809-D79B68F032EF}
11775+
{8D2CC6ED-5105-4F52-8757-C21F4DE78589} = {6469F11E-8CEE-4292-820B-324DFFC88EBC}
1175511776
{9DC6B242-457B-4767-A84B-C3D23B76C642} = {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B}
1175611777
{D53F0EF7-0CDC-49B4-AA2D-229901B0A734} = {9DC6B242-457B-4767-A84B-C3D23B76C642}
1175711778
EndGlobalSection

src/Caching/Caching.slnf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"src\\Caching\\SqlServer\\test\\Microsoft.Extensions.Caching.SqlServer.Tests.csproj",
99
"src\\Caching\\StackExchangeRedis\\src\\Microsoft.Extensions.Caching.StackExchangeRedis.csproj",
1010
"src\\Caching\\StackExchangeRedis\\test\\Microsoft.Extensions.Caching.StackExchangeRedis.Tests.csproj",
11+
"src\\Caching\\perf\\MicroBenchmarks\\Microsoft.Extensions.Caching.MicroBenchmarks\\Microsoft.Extensions.Caching.MicroBenchmarks.csproj",
1112
"src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj"
1213
]
1314
}

src/Caching/Hybrid/src/HybridCacheOptions.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@
66
using System.Linq;
77
using System.Text;
88
using System.Threading.Tasks;
9+
using Microsoft.Extensions.Options;
910

1011
namespace Microsoft.Extensions.Caching.Hybrid;
1112

1213
/// <summary>
1314
/// Options for configuring the default <see cref="HybridCache"/> implementation.
1415
/// </summary>
15-
public class HybridCacheOptions
16+
public class HybridCacheOptions // : IOptions<HybridCacheOptions>
1617
{
18+
// TODO: should we implement IOptions<T>?
19+
1720
/// <summary>
1821
/// Default global options to be applied to <see cref="HybridCache"/> operations; if options are
1922
/// specified at the individual call level, the non-null values are merged (with the per-call
@@ -45,4 +48,6 @@ public class HybridCacheOptions
4548
/// tags do not contain data that should not be visible in metrics systems.
4649
/// </summary>
4750
public bool ReportTagMetrics { get; set; }
51+
52+
// HybridCacheOptions IOptions<HybridCacheOptions>.Value => this;
4853
}

src/Caching/Hybrid/src/HybridCacheServiceExtensions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ public static IHybridCacheBuilder AddHybridCache(this IServiceCollection service
5050
services.TryAddSingleton(TimeProvider.System);
5151
services.AddOptions();
5252
services.AddMemoryCache();
53-
services.AddDistributedMemoryCache(); // we need a backend; use in-proc by default
5453
services.TryAddSingleton<IHybridCacheSerializerFactory, DefaultJsonSerializerFactory>();
5554
services.TryAddSingleton<IHybridCacheSerializer<string>>(InbuiltTypeSerializer.Instance);
5655
services.TryAddSingleton<IHybridCacheSerializer<byte[]>>(InbuiltTypeSerializer.Instance);
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Buffers;
6+
using System.Diagnostics;
7+
using System.Runtime.CompilerServices;
8+
9+
namespace Microsoft.Extensions.Caching.Hybrid.Internal;
10+
11+
// used to convey buffer status; like ArraySegment<byte>, but Offset is always
12+
// zero, and we use the MSB of the length to track whether or not to recycle this value
13+
internal readonly struct BufferChunk
14+
{
15+
private const int MSB = (1 << 31);
16+
17+
private readonly int _lengthAndPoolFlag;
18+
public byte[]? Array { get; } // null for default
19+
20+
public int Length => _lengthAndPoolFlag & ~MSB;
21+
22+
public bool ReturnToPool => (_lengthAndPoolFlag & MSB) != 0;
23+
24+
public byte[] ToArray()
25+
{
26+
var length = Length;
27+
if (length == 0)
28+
{
29+
return [];
30+
}
31+
32+
var copy = new byte[length];
33+
Buffer.BlockCopy(Array!, 0, copy, 0, length);
34+
return copy;
35+
}
36+
37+
public BufferChunk(byte[] array)
38+
{
39+
Debug.Assert(array is not null, "expected valid array input");
40+
Array = array;
41+
_lengthAndPoolFlag = array.Length;
42+
// assume not pooled, if exact-sized
43+
Debug.Assert(!ReturnToPool, "do not return right-sized arrays");
44+
Debug.Assert(Length == array.Length, "array length not respected");
45+
}
46+
47+
public BufferChunk(byte[] array, int length, bool returnToPool)
48+
{
49+
Debug.Assert(array is not null, "expected valid array input");
50+
Debug.Assert(length >= 0, "expected valid length");
51+
Array = array;
52+
_lengthAndPoolFlag = length | (returnToPool ? MSB : 0);
53+
Debug.Assert(ReturnToPool == returnToPool, "return-to-pool not respected");
54+
Debug.Assert(Length == length, "length not respected");
55+
}
56+
57+
internal void RecycleIfAppropriate()
58+
{
59+
if (ReturnToPool)
60+
{
61+
ArrayPool<byte>.Shared.Return(Array!);
62+
}
63+
Unsafe.AsRef(in this) = default; // anti foot-shotgun double-return guard; not 100%, but worth doing
64+
Debug.Assert(Array is null && !ReturnToPool, "expected clean slate after recycle");
65+
}
66+
67+
internal ReadOnlySequence<byte> AsSequence() => Length == 0 ? default : new ReadOnlySequence<byte>(Array!, 0, Length);
68+
69+
internal BufferChunk DoNotReturnToPool()
70+
{
71+
var copy = this;
72+
Unsafe.AsRef(in copy._lengthAndPoolFlag) &= ~MSB;
73+
Debug.Assert(copy.Length == Length, "same length expected");
74+
Debug.Assert(!copy.ReturnToPool, "do not return to pool");
75+
return copy;
76+
}
77+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using Microsoft.Extensions.Caching.Memory;
6+
7+
namespace Microsoft.Extensions.Caching.Hybrid.Internal;
8+
9+
partial class DefaultHybridCache
10+
{
11+
internal abstract class CacheItem
12+
{
13+
internal static readonly PostEvictionDelegate _sharedOnEviction = static (key, value, reason, state) =>
14+
{
15+
if (value is CacheItem item)
16+
{
17+
// perform per-item clean-up; this could be buffer recycling (if defensive copies needed),
18+
// or could be disposal
19+
item.OnEviction();
20+
}
21+
};
22+
23+
public virtual void Release() { } // for recycling purposes
24+
25+
public abstract bool NeedsEvictionCallback { get; } // do we need to call Release when evicted?
26+
27+
public virtual void OnEviction() { } // only invoked if NeedsEvictionCallback reported true
28+
29+
public abstract bool TryReserveBuffer(out BufferChunk buffer);
30+
31+
public abstract bool DebugIsImmutable { get; }
32+
}
33+
34+
internal abstract class CacheItem<T> : CacheItem
35+
{
36+
public abstract bool TryGetValue(out T value);
37+
38+
public T GetValue()
39+
{
40+
if (!TryGetValue(out var value))
41+
{
42+
Throw();
43+
}
44+
return value;
45+
46+
static void Throw() => throw new ObjectDisposedException("The cache item has been recycled before the value was obtained");
47+
}
48+
}
49+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Threading;
7+
8+
namespace Microsoft.Extensions.Caching.Hybrid.Internal;
9+
10+
partial class DefaultHybridCache
11+
{
12+
internal bool DebugTryGetCacheItem(string key, [NotNullWhen(true)] out CacheItem? value)
13+
{
14+
if (_localCache.TryGetValue(key, out var untyped) && untyped is CacheItem typed)
15+
{
16+
value = typed;
17+
return true;
18+
}
19+
value = null;
20+
return false;
21+
}
22+
23+
#if DEBUG // enable ref-counted buffers
24+
25+
private int _outstandingBufferCount;
26+
27+
internal int DebugGetOutstandingBuffers(bool flush = false)
28+
=> flush ? Interlocked.Exchange(ref _outstandingBufferCount, 0) : Volatile.Read(ref _outstandingBufferCount);
29+
30+
[Conditional("DEBUG")]
31+
internal void DebugDecrementOutstandingBuffers()
32+
{
33+
Interlocked.Decrement(ref _outstandingBufferCount);
34+
}
35+
36+
[Conditional("DEBUG")]
37+
internal void DebugIncrementOutstandingBuffers()
38+
{
39+
Interlocked.Increment(ref _outstandingBufferCount);
40+
}
41+
#endif
42+
43+
partial class MutableCacheItem<T>
44+
{
45+
partial void DebugDecrementOutstandingBuffers();
46+
partial void DebugTrackBufferCore(DefaultHybridCache cache);
47+
48+
[Conditional("DEBUG")]
49+
internal void DebugTrackBuffer(DefaultHybridCache cache) => DebugTrackBufferCore(cache);
50+
51+
#if DEBUG
52+
private DefaultHybridCache? _cache; // for buffer-tracking - only enabled in DEBUG
53+
partial void DebugDecrementOutstandingBuffers()
54+
{
55+
if (_buffer.ReturnToPool)
56+
{
57+
_cache?.DebugDecrementOutstandingBuffers();
58+
}
59+
}
60+
partial void DebugTrackBufferCore(DefaultHybridCache cache)
61+
{
62+
_cache = cache;
63+
if (_buffer.ReturnToPool)
64+
{
65+
_cache?.DebugIncrementOutstandingBuffers();
66+
}
67+
}
68+
#endif
69+
}
70+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using System.Diagnostics.CodeAnalysis;
7+
8+
namespace Microsoft.Extensions.Caching.Hybrid.Internal;
9+
10+
partial class DefaultHybridCache
11+
{
12+
private sealed class ImmutableCacheItem<T> : CacheItem<T> // used to hold types that do not require defensive copies
13+
{
14+
private readonly T _value;
15+
public ImmutableCacheItem(T value) => _value = value;
16+
17+
private static ImmutableCacheItem<T>? _sharedDefault;
18+
19+
// this is only used when the underlying store is disabled; we don't need 100% singleton; "good enough is"
20+
public static ImmutableCacheItem<T> Default => _sharedDefault ??= new(default!);
21+
22+
public override void OnEviction()
23+
{
24+
var obj = _value as IDisposable;
25+
Debug.Assert(obj is not null, "shouldn't be here for non-disposable types");
26+
obj?.Dispose();
27+
}
28+
29+
public override bool NeedsEvictionCallback => ImmutableTypeCache<T>.IsDisposable;
30+
31+
public override bool TryGetValue(out T value)
32+
{
33+
value = _value;
34+
return true; // always available
35+
}
36+
37+
public override bool TryReserveBuffer(out BufferChunk buffer)
38+
{
39+
buffer = default;
40+
return false; // we don't have one to reserve!
41+
}
42+
43+
public override bool DebugIsImmutable => true;
44+
}
45+
}

0 commit comments

Comments
 (0)