Skip to content

Commit b27c028

Browse files
authored
HybridCache - minor post-PR fixes (#55251)
* additional HybridCache post-PR fixes
1 parent 1232b89 commit b27c028

23 files changed

+511
-367
lines changed

src/Caching/Hybrid/src/HybridCacheBuilderExtensions.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Collections.Generic;
64
using System.Diagnostics.CodeAnalysis;
7-
using System.Linq;
8-
using System.Text;
9-
using System.Threading.Tasks;
105
using Microsoft.Extensions.DependencyInjection;
116

127
namespace Microsoft.Extensions.Caching.Hybrid;

src/Caching/Hybrid/src/HybridCacheOptions.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Linq;
7-
using System.Text;
8-
using System.Threading.Tasks;
9-
using Microsoft.Extensions.Options;
10-
114
namespace Microsoft.Extensions.Caching.Hybrid;
125

136
/// <summary>

src/Caching/Hybrid/src/HybridCacheServiceExtensions.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5-
using System.Collections.Generic;
6-
using System.Diagnostics.CodeAnalysis;
7-
using System.Linq;
8-
using System.Text;
9-
using System.Threading.Tasks;
105
using Microsoft.Extensions.Caching.Hybrid.Internal;
116
using Microsoft.Extensions.DependencyInjection;
127
using Microsoft.Extensions.DependencyInjection.Extensions;
13-
using Microsoft.Extensions.Internal;
148

159
namespace Microsoft.Extensions.Caching.Hybrid;
1610

src/Caching/Hybrid/src/IHybridCacheBuilder.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Linq;
7-
using System.Text;
8-
using System.Threading.Tasks;
94
using Microsoft.Extensions.DependencyInjection;
105

116
namespace Microsoft.Extensions.Caching.Hybrid;

src/Caching/Hybrid/src/Internal/BufferChunk.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
namespace Microsoft.Extensions.Caching.Hybrid.Internal;
1010

1111
// 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
12+
// zero, and we use the most significant bit (MSB, the sign flag) of the length
13+
// to track whether or not to recycle this value
1314
internal readonly struct BufferChunk
1415
{
1516
private const int MSB = (1 << 31);
@@ -40,6 +41,10 @@ public BufferChunk(byte[] array)
4041
Array = array;
4142
_lengthAndPoolFlag = array.Length;
4243
// assume not pooled, if exact-sized
44+
// (we don't expect array.Length to be negative; we're really just saying
45+
// "we expect the result of assigning array.Length to _lengthAndPoolFlag
46+
// to give the expected Length *and* not have the MSB set; we're just
47+
// checking that we haven't fat-fingered our MSB logic)
4348
Debug.Assert(!ReturnToPool, "do not return right-sized arrays");
4449
Debug.Assert(Length == array.Length, "array length not respected");
4550
}

src/Caching/Hybrid/src/Internal/DefaultHybridCache.CacheItem.cs

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Diagnostics;
6+
using System.Threading;
57
using Microsoft.Extensions.Caching.Memory;
68

79
namespace Microsoft.Extensions.Caching.Hybrid.Internal;
@@ -10,37 +12,85 @@ partial class DefaultHybridCache
1012
{
1113
internal abstract class CacheItem
1214
{
15+
private int _refCount = 1; // the number of pending operations against this cache item
16+
// note: the ref count is the number of callers anticipating this value at any given time; initially,
17+
// it is one for a simple "get the value" flow, but if another call joins with us, it'll be incremented;
18+
// if either cancels, it will get decremented, with the entire flow being cancelled if it ever becomes
19+
// zero
20+
// this counter also drives cache lifetime, with the cache itself incrementing the count by one; in the
21+
// case of mutable data, cache eviction may reduce this to zero (in cooperation with any concurrent readers,
22+
// who incr/decr around their fetch), allowing safe buffer recycling
23+
24+
internal int RefCount => Volatile.Read(ref _refCount);
25+
1326
internal static readonly PostEvictionDelegate _sharedOnEviction = static (key, value, reason, state) =>
1427
{
1528
if (value is CacheItem item)
1629
{
17-
// perform per-item clean-up; this could be buffer recycling (if defensive copies needed),
18-
// or could be disposal
19-
item.OnEviction();
30+
item.Release();
2031
}
2132
};
2233

23-
public virtual void Release() { } // for recycling purposes
34+
public virtual bool NeedsEvictionCallback => false; // do we need to call Release when evicted?
2435

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
36+
protected virtual void OnFinalRelease() { } // any required release semantics
2837

2938
public abstract bool TryReserveBuffer(out BufferChunk buffer);
3039

3140
public abstract bool DebugIsImmutable { get; }
41+
42+
public bool Release() // returns true ONLY for the final release step
43+
{
44+
var newCount = Interlocked.Decrement(ref _refCount);
45+
Debug.Assert(newCount >= 0, "over-release detected");
46+
if (newCount == 0)
47+
{
48+
// perform per-item clean-up, i.e. buffer recycling (if defensive copies needed)
49+
OnFinalRelease();
50+
return true;
51+
}
52+
return false;
53+
}
54+
55+
public bool TryReserve()
56+
{
57+
// this is basically interlocked increment, but with a check against:
58+
// a) incrementing upwards from zero
59+
// b) overflowing *back* to zero
60+
var oldValue = Volatile.Read(ref _refCount);
61+
do
62+
{
63+
if (oldValue is 0 or -1)
64+
{
65+
return false; // already burned, or about to roll around back to zero
66+
}
67+
68+
var updated = Interlocked.CompareExchange(ref _refCount, oldValue + 1, oldValue);
69+
if (updated == oldValue)
70+
{
71+
return true; // we exchanged
72+
}
73+
oldValue = updated; // we failed, but we have an updated state
74+
} while (true);
75+
}
76+
3277
}
3378

3479
internal abstract class CacheItem<T> : CacheItem
3580
{
81+
internal static CacheItem<T> Create() => ImmutableTypeCache<T>.IsImmutable ? new ImmutableCacheItem<T>() : new MutableCacheItem<T>();
82+
83+
// attempt to get a value that was *not* previously reserved
3684
public abstract bool TryGetValue(out T value);
3785

38-
public T GetValue()
86+
// get a value that *was* reserved, countermanding our reservation in the process
87+
public T GetReservedValue()
3988
{
4089
if (!TryGetValue(out var value))
4190
{
4291
Throw();
4392
}
93+
Release();
4494
return value;
4595

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

src/Caching/Hybrid/src/Internal/DefaultHybridCache.Debug.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,45 +24,45 @@ internal bool DebugTryGetCacheItem(string key, [NotNullWhen(true)] out CacheItem
2424

2525
private int _outstandingBufferCount;
2626

27-
internal int DebugGetOutstandingBuffers(bool flush = false)
27+
internal int DebugOnlyGetOutstandingBuffers(bool flush = false)
2828
=> flush ? Interlocked.Exchange(ref _outstandingBufferCount, 0) : Volatile.Read(ref _outstandingBufferCount);
2929

3030
[Conditional("DEBUG")]
31-
internal void DebugDecrementOutstandingBuffers()
31+
internal void DebugOnlyDecrementOutstandingBuffers()
3232
{
3333
Interlocked.Decrement(ref _outstandingBufferCount);
3434
}
3535

3636
[Conditional("DEBUG")]
37-
internal void DebugIncrementOutstandingBuffers()
37+
internal void DebugOnlyIncrementOutstandingBuffers()
3838
{
3939
Interlocked.Increment(ref _outstandingBufferCount);
4040
}
4141
#endif
4242

4343
partial class MutableCacheItem<T>
4444
{
45-
partial void DebugDecrementOutstandingBuffers();
46-
partial void DebugTrackBufferCore(DefaultHybridCache cache);
45+
partial void DebugOnlyDecrementOutstandingBuffers();
46+
partial void DebugOnlyTrackBufferCore(DefaultHybridCache cache);
4747

4848
[Conditional("DEBUG")]
49-
internal void DebugTrackBuffer(DefaultHybridCache cache) => DebugTrackBufferCore(cache);
49+
internal void DebugOnlyTrackBuffer(DefaultHybridCache cache) => DebugOnlyTrackBufferCore(cache);
5050

5151
#if DEBUG
5252
private DefaultHybridCache? _cache; // for buffer-tracking - only enabled in DEBUG
53-
partial void DebugDecrementOutstandingBuffers()
53+
partial void DebugOnlyDecrementOutstandingBuffers()
5454
{
5555
if (_buffer.ReturnToPool)
5656
{
57-
_cache?.DebugDecrementOutstandingBuffers();
57+
_cache?.DebugOnlyDecrementOutstandingBuffers();
5858
}
5959
}
60-
partial void DebugTrackBufferCore(DefaultHybridCache cache)
60+
partial void DebugOnlyTrackBufferCore(DefaultHybridCache cache)
6161
{
6262
_cache = cache;
6363
if (_buffer.ReturnToPool)
6464
{
65-
_cache?.DebugIncrementOutstandingBuffers();
65+
_cache?.DebugOnlyIncrementOutstandingBuffers();
6666
}
6767
}
6868
#endif

src/Caching/Hybrid/src/Internal/DefaultHybridCache.ImmutableCacheItem.cs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Diagnostics;
6-
using System.Diagnostics.CodeAnalysis;
4+
using System.Threading;
75

86
namespace Microsoft.Extensions.Caching.Hybrid.Internal;
97

108
partial class DefaultHybridCache
119
{
1210
private sealed class ImmutableCacheItem<T> : CacheItem<T> // used to hold types that do not require defensive copies
1311
{
14-
private readonly T _value;
15-
public ImmutableCacheItem(T value) => _value = value;
12+
private T _value = default!; // deferred until SetValue
1613

17-
private static ImmutableCacheItem<T>? _sharedDefault;
14+
public void SetValue(T value) => _value = value;
1815

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!);
16+
private static ImmutableCacheItem<T>? _sharedDefault;
2117

22-
public override void OnEviction()
18+
// get a shared instance that passes as "reserved"; doesn't need to be 100% singleton,
19+
// but we don't want to break the reservation rules either; if we can't reserve: create new
20+
public static ImmutableCacheItem<T> GetReservedShared()
2321
{
24-
var obj = _value as IDisposable;
25-
Debug.Assert(obj is not null, "shouldn't be here for non-disposable types");
26-
obj?.Dispose();
22+
var obj = Volatile.Read(ref _sharedDefault);
23+
if (obj is null || !obj.TryReserve())
24+
{
25+
obj = new();
26+
obj.TryReserve(); // this is reliable on a new instance
27+
Volatile.Write(ref _sharedDefault, obj);
28+
}
29+
return obj;
2730
}
2831

29-
public override bool NeedsEvictionCallback => ImmutableTypeCache<T>.IsDisposable;
30-
3132
public override bool TryGetValue(out T value)
3233
{
3334
value = _value;

src/Caching/Hybrid/src/Internal/DefaultHybridCache.L2.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,16 @@ private DistributedCacheEntryOptions GetOptions(HybridCacheEntryOptions? options
113113

114114
internal void SetL1<T>(string key, CacheItem<T> value, HybridCacheEntryOptions? options)
115115
{
116-
// based on CacheExtensions.Set<TItem>, but with post-eviction recycling
117-
using var cacheEntry = _localCache.CreateEntry(key);
118-
cacheEntry.AbsoluteExpirationRelativeToNow = options?.LocalCacheExpiration ?? _defaultLocalCacheExpiration;
119-
cacheEntry.Value = value;
120-
if (value.NeedsEvictionCallback)
116+
if (value.TryReserve()) // incr ref-count for the the cache itself; this *may* be released via the NeedsEvictionCallback path
121117
{
122-
cacheEntry.RegisterPostEvictionCallback(CacheItem._sharedOnEviction);
118+
// based on CacheExtensions.Set<TItem>, but with post-eviction recycling
119+
using var cacheEntry = _localCache.CreateEntry(key);
120+
cacheEntry.AbsoluteExpirationRelativeToNow = options?.LocalCacheExpiration ?? _defaultLocalCacheExpiration;
121+
cacheEntry.Value = value;
122+
if (value.NeedsEvictionCallback)
123+
{
124+
cacheEntry.RegisterPostEvictionCallback(CacheItem._sharedOnEviction);
125+
}
123126
}
124127
}
125128
}

src/Caching/Hybrid/src/Internal/DefaultHybridCache.MutableCacheItem.cs

Lines changed: 7 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Diagnostics;
6-
using System.Threading;
7-
84
namespace Microsoft.Extensions.Caching.Hybrid.Internal;
95

106
partial class DefaultHybridCache
117
{
128
private sealed partial class MutableCacheItem<T> : CacheItem<T> // used to hold types that require defensive copies
139
{
14-
private readonly IHybridCacheSerializer<T> _serializer;
15-
private readonly BufferChunk _buffer;
16-
private int _refCount = 1; // buffer released when this becomes zero
10+
private IHybridCacheSerializer<T> _serializer = null!; // deferred until SetValue
11+
private BufferChunk _buffer;
1712

18-
public MutableCacheItem(ref BufferChunk buffer, IHybridCacheSerializer<T> serializer)
13+
public void SetValue(ref BufferChunk buffer, IHybridCacheSerializer<T> serializer)
1914
{
2015
_serializer = serializer;
2116
_buffer = buffer;
2217
buffer = default; // we're taking over the lifetime; the caller no longer has it!
2318
}
2419

25-
public MutableCacheItem(T value, IHybridCacheSerializer<T> serializer, int maxLength)
20+
public void SetValue(T value, IHybridCacheSerializer<T> serializer, int maxLength)
2621
{
2722
_serializer = serializer;
2823
var writer = RecyclableArrayBufferWriter<byte>.Create(maxLength);
@@ -34,35 +29,10 @@ public MutableCacheItem(T value, IHybridCacheSerializer<T> serializer, int maxLe
3429

3530
public override bool NeedsEvictionCallback => _buffer.ReturnToPool;
3631

37-
public override void OnEviction() => Release();
38-
39-
public override void Release()
32+
protected override void OnFinalRelease()
4033
{
41-
var newCount = Interlocked.Decrement(ref _refCount);
42-
if (newCount == 0)
43-
{
44-
DebugDecrementOutstandingBuffers();
45-
_buffer.RecycleIfAppropriate();
46-
}
47-
}
48-
49-
public bool TryReserve()
50-
{
51-
var oldValue = Volatile.Read(ref _refCount);
52-
do
53-
{
54-
if (oldValue is 0 or -1)
55-
{
56-
return false; // already burned, or about to roll around back to zero
57-
}
58-
59-
var updated = Interlocked.CompareExchange(ref _refCount, oldValue + 1, oldValue);
60-
if (updated == oldValue)
61-
{
62-
return true; // we exchanged
63-
}
64-
oldValue = updated; // we failed, but we have an updated state
65-
} while (true);
34+
DebugOnlyDecrementOutstandingBuffers();
35+
_buffer.RecycleIfAppropriate();
6636
}
6737

6838
public override bool TryGetValue(out T value)

0 commit comments

Comments
 (0)