-
Notifications
You must be signed in to change notification settings - Fork 10.4k
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
Changes from all commits
35e9cea
12b3a0e
c5d28d4
20e1cb2
4379647
d382a7a
e8bc9a9
7edddb1
dc622a8
4920182
8841e97
6cbe02e
0c0d589
ef0ad56
e28f316
fbedef4
b49151e
c2326fd
29dcc2e
4ddba7f
427601c
cef964f
6f572f3
a41175c
82a34e3
e3a9173
373aaa8
8c1cc9e
40e2c47
cede0e9
ad88ef1
e427ad9
b9bc68a
1d548bf
18ae584
e60099b
5421398
c3adf74
0ac4dae
1db2758
2945ce0
94d9fe1
a13e1d3
2a77920
cee8ddc
7989f16
49a477f
5f8bcb8
7b32e0f
62b4e6f
c6cdeae
d624726
e831198
83b7a1d
59bc62a
0e36776
ee15beb
eb7a294
ffa0e0a
1b12e1a
3e42524
8c0e1b5
6c10b8d
552676f
bb1f856
6a25601
17383c4
14bb5fc
e6b2cca
96d75fa
366bd82
256581b
45d31b3
3de225e
c796f52
ce7ec84
0489dce
e5a080f
de2a4b0
d4133d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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"); | ||
mgravell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
amcasey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Debug.Assert(Array is null && !ReturnToPool, "expected clean slate after recycle"); | ||
mgravell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
internal ReadOnlySequence<byte> AsSequence() => Length == 0 ? default : new ReadOnlySequence<byte>(Array!, 0, Length); | ||
|
||
internal BufferChunk DoNotReturnToPool() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'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; | ||
amcasey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Debug.Assert(copy.Length == Length, "same length expected"); | ||
Debug.Assert(!copy.ReturnToPool, "do not return to pool"); | ||
return copy; | ||
} | ||
} |
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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why pull this out? Something about perf? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It still holds. 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 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? |
||
} | ||
} | ||
} |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.