Skip to content

Implement minimal implementation of HybridCache #55147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 80 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
35e9cea
initial API cut (post review)
mgravell Apr 10, 2024
12b3a0e
basic API test
mgravell Apr 11, 2024
c5d28d4
prove that the API can be configured
mgravell Apr 11, 2024
20e1cb2
demonstrate serializer/factory configuration working
mgravell Apr 11, 2024
4379647
move to NuGet only to make the build happier
mgravell Apr 11, 2024
d382a7a
defer on trimming
mgravell Apr 11, 2024
e8bc9a9
PR review comments
mgravell Apr 11, 2024
7edddb1
tyop
mgravell Apr 11, 2024
dc622a8
Update src/Caching/Hybrid/src/Runtime/IsExternalInit.cs
mgravell Apr 11, 2024
4920182
return a leased array on netfx
mgravell Apr 11, 2024
8841e97
prefer ForEach to Walk
mgravell Apr 11, 2024
6cbe02e
Update src/Caching/Hybrid/src/Internal/InbuiltTypeSerializer.cs
mgravell Apr 11, 2024
0c0d589
Update src/Caching/Hybrid/src/Internal/DefaultJsonSerializerFactory.cs
mgravell Apr 11, 2024
ef0ad56
Update src/Caching/Hybrid/src/Internal/InbuiltTypeSerializer.cs
mgravell Apr 11, 2024
e28f316
comment nits
mgravell Apr 11, 2024
fbedef4
Merge branch 'marc/hybrid-api' of https://github.com/dotnet/aspnetcor…
mgravell Apr 11, 2024
b49151e
use TimeProvider throughout
mgravell Apr 11, 2024
c2326fd
more nits
mgravell Apr 11, 2024
29dcc2e
regen projects list
mgravell Apr 11, 2024
4ddba7f
TryAdd
mgravell Apr 12, 2024
427601c
clarify intent of null on Remove{Tags|Keys}Async
mgravell Apr 12, 2024
cef964f
basic stampede infrastructure
mgravell Apr 12, 2024
6f572f3
streamline the non-canceled scenario
mgravell Apr 15, 2024
a41175c
L2
mgravell Apr 15, 2024
82a34e3
L1
mgravell Apr 15, 2024
e3a9173
L2 tests
mgravell Apr 15, 2024
373aaa8
mutable/immutable tests
mgravell Apr 15, 2024
8c1cc9e
build warnings
mgravell Apr 15, 2024
40e2c47
implement SetValueAsync
mgravell Apr 15, 2024
cede0e9
support ns2.1
mgravell Apr 15, 2024
ad88ef1
prove immutable type behaviours
mgravell Apr 16, 2024
e427ad9
TFM summary
mgravell Apr 16, 2024
b9bc68a
make L2 optional; fast-path L2
mgravell Apr 16, 2024
1d548bf
redis tests
mgravell Apr 16, 2024
18ae584
implement streaming via SqlClient
mgravell Apr 16, 2024
e60099b
add benchmark project
mgravell Apr 17, 2024
5421398
nit
mgravell Apr 17, 2024
c3adf74
fixes
mgravell Apr 17, 2024
0ac4dae
nit
mgravell Apr 17, 2024
1db2758
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
2945ce0
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
94d9fe1
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
a13e1d3
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
2a77920
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
cee8ddc
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
7989f16
Update src/Caching/Hybrid/src/Runtime/HybridCacheEntryFlags.cs
mgravell Apr 17, 2024
49a477f
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
5f8bcb8
Update src/Caching/Hybrid/src/Runtime/HybridCacheEntryOptions.cs
mgravell Apr 17, 2024
7b32e0f
Update src/Caching/Hybrid/src/Runtime/HybridCache.cs
mgravell Apr 17, 2024
62b4e6f
Update src/Caching/Hybrid/src/Runtime/IBufferDistributedCache.cs
mgravell Apr 17, 2024
c6cdeae
Apply suggestions from code review
mgravell Apr 17, 2024
d624726
more PR feedback
mgravell Apr 17, 2024
e831198
Merge branch 'marc/hybrid-api' into marc/hybrid-api-basic
mgravell Apr 17, 2024
83b7a1d
apply _ field convention
mgravell Apr 17, 2024
59bc62a
nit comment
mgravell Apr 17, 2024
0e36776
add tests for advanced buffer scenarios for redis+sql
mgravell Apr 17, 2024
ee15beb
Empty-Commit
mgravell Apr 17, 2024
eb7a294
Merge branch 'main' into marc/hybrid-api-basic
mgravell Apr 17, 2024
ffa0e0a
hybrid cache baseline perf
mgravell Apr 17, 2024
1b12e1a
move benchmarks ala PR feedback
mgravell Apr 18, 2024
3e42524
include max allowed payload size in fault
mgravell Apr 18, 2024
8c0e1b5
add comment on the serializer cache
mgravell Apr 18, 2024
6c10b8d
include type data in the wrong-type message
mgravell Apr 18, 2024
552676f
constify
mgravell Apr 18, 2024
bb1f856
missing lic headers
mgravell Apr 18, 2024
6a25601
use standard Try* pattern
mgravell Apr 18, 2024
17383c4
redundant ?.
mgravell Apr 18, 2024
14bb5fc
TCS: async completions
mgravell Apr 18, 2024
e6b2cca
use double-checked lock to avoid race conditions creating two concurr…
mgravell Apr 18, 2024
96d75fa
move state above methods; use NullLogger
mgravell Apr 18, 2024
366bd82
Update src/Caching/Hybrid/src/Internal/DefaultHybridCache.StampedeSta…
mgravell Apr 18, 2024
256581b
add proper buffer recycling of buffers inside cache items
mgravell Apr 18, 2024
45d31b3
Merge branch 'marc/hybrid-api-basic' of https://github.com/dotnet/asp…
mgravell Apr 18, 2024
3de225e
prefer HashCode when possible
mgravell Apr 18, 2024
c796f52
Merge branch 'main' into marc/hybrid-api-basic
mgravell Apr 18, 2024
ce7ec84
fix bad merge (need longer name for disambiguation)
mgravell Apr 18, 2024
0489dce
look into CI failure; timing
mgravell Apr 19, 2024
e5a080f
nits
mgravell Apr 19, 2024
de2a4b0
disposable support, and fix IDE analyzer nits
mgravell Apr 19, 2024
d4133d1
remove IOptions<T> for now
mgravell Apr 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions AspNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -1796,6 +1796,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Cachin
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}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{9DC6B242-457B-4767-A84B-C3D23B76C642}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi.Microbenchmarks", "src\OpenApi\perf\Microbenchmarks\Microsoft.AspNetCore.OpenApi.Microbenchmarks.csproj", "{D53F0EF7-0CDC-49B4-AA2D-229901B0A734}"
Expand Down Expand Up @@ -10849,6 +10852,22 @@ Global
{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
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|arm64.Build.0 = Debug|Any CPU
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|x64.ActiveCfg = Debug|Any CPU
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|x64.Build.0 = Debug|Any CPU
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|x86.ActiveCfg = Debug|Any CPU
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Debug|x86.Build.0 = Debug|Any CPU
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|Any CPU.Build.0 = Release|Any CPU
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|arm64.ActiveCfg = Release|Any CPU
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|arm64.Build.0 = Release|Any CPU
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|x64.ActiveCfg = Release|Any CPU
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|x64.Build.0 = Release|Any CPU
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|x86.ActiveCfg = Release|Any CPU
{8D2CC6ED-5105-4F52-8757-C21F4DE78589}.Release|x86.Build.0 = Release|Any CPU
{D53F0EF7-0CDC-49B4-AA2D-229901B0A734}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D53F0EF7-0CDC-49B4-AA2D-229901B0A734}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D53F0EF7-0CDC-49B4-AA2D-229901B0A734}.Debug|arm64.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -11752,6 +11771,8 @@ Global
{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}
{D53F0EF7-0CDC-49B4-AA2D-229901B0A734} = {9DC6B242-457B-4767-A84B-C3D23B76C642}
EndGlobalSection
Expand Down
1 change: 1 addition & 0 deletions src/Caching/Caching.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"src\\Caching\\SqlServer\\test\\Microsoft.Extensions.Caching.SqlServer.Tests.csproj",
"src\\Caching\\StackExchangeRedis\\src\\Microsoft.Extensions.Caching.StackExchangeRedis.csproj",
"src\\Caching\\StackExchangeRedis\\test\\Microsoft.Extensions.Caching.StackExchangeRedis.Tests.csproj",
"src\\Caching\\perf\\MicroBenchmarks\\Microsoft.Extensions.Caching.MicroBenchmarks\\Microsoft.Extensions.Caching.MicroBenchmarks.csproj",
"src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj"
]
}
Expand Down
7 changes: 6 additions & 1 deletion src/Caching/Hybrid/src/HybridCacheOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.Caching.Hybrid;

/// <summary>
/// Options for configuring the default <see cref="HybridCache"/> implementation.
/// </summary>
public class HybridCacheOptions
public class HybridCacheOptions // : IOptions<HybridCacheOptions>
{
// TODO: should we implement IOptions<T>?

/// <summary>
/// Default global options to be applied to <see cref="HybridCache"/> operations; if options are
/// specified at the individual call level, the non-null values are merged (with the per-call
Expand Down Expand Up @@ -45,4 +48,6 @@ public class HybridCacheOptions
/// tags do not contain data that should not be visible in metrics systems.
/// </summary>
public bool ReportTagMetrics { get; set; }

// HybridCacheOptions IOptions<HybridCacheOptions>.Value => this;
}
1 change: 0 additions & 1 deletion src/Caching/Hybrid/src/HybridCacheServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ public static IHybridCacheBuilder AddHybridCache(this IServiceCollection service
services.TryAddSingleton(TimeProvider.System);
services.AddOptions();
services.AddMemoryCache();
services.AddDistributedMemoryCache(); // we need a backend; use in-proc by default
services.TryAddSingleton<IHybridCacheSerializerFactory, DefaultJsonSerializerFactory>();
services.TryAddSingleton<IHybridCacheSerializer<string>>(InbuiltTypeSerializer.Instance);
services.TryAddSingleton<IHybridCacheSerializer<byte[]>>(InbuiltTypeSerializer.Instance);
Expand Down
77 changes: 77 additions & 0 deletions src/Caching/Hybrid/src/Internal/BufferChunk.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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<byte>, but Offset is always
// zero, and we use the MSB 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
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<byte>.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<byte> AsSequence() => Length == 0 ? default : new ReadOnlySequence<byte>(Array!, 0, Length);

internal BufferChunk DoNotReturnToPool()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure this makes a clone with the "return to pool" bit cleared and leaves this copy as-is?

{
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;
}
}
49 changes: 49 additions & 0 deletions src/Caching/Hybrid/src/Internal/DefaultHybridCache.CacheItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// 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.Memory;

namespace Microsoft.Extensions.Caching.Hybrid.Internal;

partial class DefaultHybridCache
{
internal abstract class CacheItem
{
internal static readonly PostEvictionDelegate _sharedOnEviction = static (key, value, reason, state) =>
{
if (value is CacheItem item)
{
// perform per-item clean-up; this could be buffer recycling (if defensive copies needed),
// or could be disposal
item.OnEviction();
}
};

public virtual void Release() { } // for recycling purposes

public abstract bool NeedsEvictionCallback { get; } // do we need to call Release when evicted?

public virtual void OnEviction() { } // only invoked if NeedsEvictionCallback reported true

public abstract bool TryReserveBuffer(out BufferChunk buffer);

public abstract bool DebugIsImmutable { get; }
}

internal abstract class CacheItem<T> : CacheItem
{
public abstract bool TryGetValue(out T value);

public T GetValue()
{
if (!TryGetValue(out var value))
{
Throw();
}
return value;

static void Throw() => throw new ObjectDisposedException("The cache item has been recycled before the value was obtained");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why pull this out? Something about perf?

Copy link
Member

@ReubenBond ReubenBond Apr 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, it lets the calling method be inlined into its caller. See the "Use static throw helpers" section here: https://reubenbond.github.io/posts/dotnet-perf-tuning. I am not certain if the advice still holds, given all of the advancements in the JIT in the last 5 years, but I still generally manually split out the rare, expensive paths to make methods easier to inline

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not certain if the advice still holds

It still holds.
With JIT's PGO capabilities the cold pathes would be moved to a cold block of machine code, but it's not split into a separate method. Thus manually splitting these rare paths into a separate method makes sense, as the hot-/happy-path method gets smaller and is potentially inlined.

For non-throwing rare path methods they can be marked with non-inlining, to make sure that the JIT won't inline these methods by coincidence.
For throwing methods non-inlining shouldn't be applied, in order for the JIT to see that the method won't return (because of the throw). If at these methods non-inlining would be applied the JIT wouldn't examine the method at all, and just emit a call to it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason the JIT doesn't inline methods with throws is that it wants to keep them on the stack in the event of an exception?

}
}
}
70 changes: 70 additions & 0 deletions src/Caching/Hybrid/src/Internal/DefaultHybridCache.Debug.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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 DebugGetOutstandingBuffers(bool flush = false)
=> flush ? Interlocked.Exchange(ref _outstandingBufferCount, 0) : Volatile.Read(ref _outstandingBufferCount);

[Conditional("DEBUG")]
internal void DebugDecrementOutstandingBuffers()
{
Interlocked.Decrement(ref _outstandingBufferCount);
}

[Conditional("DEBUG")]
internal void DebugIncrementOutstandingBuffers()
{
Interlocked.Increment(ref _outstandingBufferCount);
}
#endif

partial class MutableCacheItem<T>
{
partial void DebugDecrementOutstandingBuffers();
partial void DebugTrackBufferCore(DefaultHybridCache cache);

[Conditional("DEBUG")]
internal void DebugTrackBuffer(DefaultHybridCache cache) => DebugTrackBufferCore(cache);

#if DEBUG
private DefaultHybridCache? _cache; // for buffer-tracking - only enabled in DEBUG
partial void DebugDecrementOutstandingBuffers()
{
if (_buffer.ReturnToPool)
{
_cache?.DebugDecrementOutstandingBuffers();
}
}
partial void DebugTrackBufferCore(DefaultHybridCache cache)
{
_cache = cache;
if (_buffer.ReturnToPool)
{
_cache?.DebugIncrementOutstandingBuffers();
}
}
#endif
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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;

namespace Microsoft.Extensions.Caching.Hybrid.Internal;

partial class DefaultHybridCache
{
private sealed class ImmutableCacheItem<T> : CacheItem<T> // used to hold types that do not require defensive copies
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it a doc comment? (And below.)

{
private readonly T _value;
public ImmutableCacheItem(T value) => _value = value;

private static ImmutableCacheItem<T>? _sharedDefault;

// this is only used when the underlying store is disabled; we don't need 100% singleton; "good enough is"
public static ImmutableCacheItem<T> Default => _sharedDefault ??= new(default!);

public override void OnEviction()
{
var obj = _value as IDisposable;
Debug.Assert(obj is not null, "shouldn't be here for non-disposable types");
obj?.Dispose();
}

public override bool NeedsEvictionCallback => ImmutableTypeCache<T>.IsDisposable;

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;
}
}
Loading