Skip to content

Commit 0489dce

Browse files
committed
look into CI failure; timing
1 parent ce7ec84 commit 0489dce

File tree

4 files changed

+190
-30
lines changed

4 files changed

+190
-30
lines changed

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,32 @@ internal void DebugIncrementOutstandingBuffers()
3939
Interlocked.Increment(ref _outstandingBufferCount);
4040
}
4141
#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+
}
4270
}

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

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,11 @@ namespace Microsoft.Extensions.Caching.Hybrid.Internal;
99

1010
partial class DefaultHybridCache
1111
{
12-
private sealed class MutableCacheItem<T> : CacheItem<T> // used to hold types that require defensive copies
12+
private sealed partial class MutableCacheItem<T> : CacheItem<T> // used to hold types that require defensive copies
1313
{
1414
private readonly IHybridCacheSerializer<T> _serializer;
1515
private readonly BufferChunk _buffer;
1616
private int _refCount = 1; // buffer released when this becomes zero
17-
#if DEBUG
18-
private DefaultHybridCache? _cache; // for buffer-tracking - only enabled in DEBUG
19-
internal void DebugTrackBuffer(DefaultHybridCache cache)
20-
{
21-
_cache = cache;
22-
if (_buffer.ReturnToPool)
23-
{
24-
_cache.DebugIncrementOutstandingBuffers();
25-
}
26-
}
27-
#endif
2817

2918
public MutableCacheItem(ref BufferChunk buffer, IHybridCacheSerializer<T> serializer)
3019
{
@@ -43,19 +32,14 @@ public MutableCacheItem(T value, IHybridCacheSerializer<T> serializer, int maxLe
4332
writer.Dispose(); // no buffers left (we just detached them), but just in case of other logic
4433
}
4534

46-
public override bool NeedsEvictionCallback => true;
35+
public override bool NeedsEvictionCallback => _buffer.ReturnToPool;
4736

4837
public override void Release()
4938
{
5039
var newCount = Interlocked.Decrement(ref _refCount);
5140
if (newCount == 0)
5241
{
53-
#if DEBUG // avoid even the property touch if not in debug
54-
if (_buffer.ReturnToPool)
55-
{
56-
_cache?.DebugDecrementOutstandingBuffers();
57-
}
58-
#endif
42+
DebugDecrementOutstandingBuffers();
5943
_buffer.RecycleIfAppropriate();
6044
}
6145
}

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,7 @@ private void SetResultAndRecycleIfAppropriate(ref BufferChunk value)
180180
{
181181
// use the buffer directly as the backing in the cache-item; do *not* recycle now
182182
var tmp = new MutableCacheItem<T>(ref value, serializer);
183-
#if DEBUG
184-
tmp.DebugTrackBuffer(Cache);
185-
#endif
183+
tmp.DebugTrackBuffer(Cache); // conditional: DEBUG
186184
cacheItem = tmp;
187185
}
188186

@@ -200,9 +198,7 @@ private CacheItem<T> SetResult(T value)
200198
else
201199
{
202200
var tmp = new MutableCacheItem<T>(value, Cache.GetSerializer<T>(), MaximumPayloadBytes); // serialization happens here
203-
#if DEBUG
204-
tmp.DebugTrackBuffer(Cache);
205-
#endif
201+
tmp.DebugTrackBuffer(Cache); // conditional: DEBUG
206202
cacheItem = tmp;
207203
}
208204
SetResult(cacheItem);

src/Caching/Hybrid/test/BufferReleaseTests.cs

Lines changed: 157 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,31 @@
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.Buffers;
45
using System.Runtime.CompilerServices;
6+
using Microsoft.Extensions.Caching.Distributed;
57
using Microsoft.Extensions.Caching.Hybrid.Internal;
8+
using Microsoft.Extensions.Caching.Memory;
69
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Options;
711
using static Microsoft.Extensions.Caching.Hybrid.Internal.DefaultHybridCache;
812

913
namespace Microsoft.Extensions.Caching.Hybrid.Tests;
1014

1115
public class BufferReleaseTests // note that buffer ref-counting is only enabled for DEBUG builds; can only verify general behaviour without that
1216
{
13-
static IDisposable GetDefaultCache(out DefaultHybridCache cache)
17+
static IDisposable GetDefaultCache(out DefaultHybridCache cache, Action<ServiceCollection>? config = null)
1418
{
1519
var services = new ServiceCollection();
20+
config?.Invoke(services);
1621
services.AddHybridCache();
1722
var provider = services.BuildServiceProvider();
1823
cache = Assert.IsType<DefaultHybridCache>(provider.GetRequiredService<HybridCache>());
1924
return provider;
2025
}
2126

2227
[Fact]
23-
public async Task BufferGetsReleased()
28+
public async Task BufferGetsReleased_NoL2()
2429
{
2530
using var provider = GetDefaultCache(out var cache);
2631
#if DEBUG
@@ -39,27 +44,174 @@ public async Task BufferGetsReleased()
3944
Assert.True(cache.DebugTryGetCacheItem(key, out var cacheItem));
4045

4146
// assert that we can reserve the buffer *now* (mostly to see that it behaves differently later)
47+
Assert.True(cacheItem.NeedsEvictionCallback, "should be pooled memory");
4248
Assert.True(cacheItem.TryReserveBuffer(out _));
4349
cacheItem.Release(); // for the above reserve
4450

45-
var second = await cache.GetOrCreateAsync(key, _ => GetAsync(), new HybridCacheEntryOptions { Flags = HybridCacheEntryFlags.DisableUnderlyingData });
51+
var second = await cache.GetOrCreateAsync(key, _ => GetAsync(), NoUnderlying);
4652
Assert.NotNull(second);
4753
Assert.NotSame(first, second);
4854

4955
await cache.RemoveKeyAsync(key);
50-
var third = await cache.GetOrCreateAsync(key, _ => GetAsync(), new HybridCacheEntryOptions { Flags = HybridCacheEntryFlags.DisableUnderlyingData });
56+
var third = await cache.GetOrCreateAsync(key, _ => GetAsync(), NoUnderlying);
5157
Assert.Null(third);
5258

53-
await Task.Delay(500); // give it a moment
59+
// give it a moment for the eviction callback to kick in
60+
for (int i = 0; i < 10 && cacheItem.NeedsEvictionCallback; i++)
61+
{
62+
await Task.Delay(250);
63+
}
5464
#if DEBUG
5565
Assert.Equal(0, cache.DebugGetOutstandingBuffers());
5666
#endif
5767
// assert that we can *no longer* reserve this buffer, because we've already recycled it
5868
Assert.False(cacheItem.TryReserveBuffer(out _));
69+
Assert.False(cacheItem.NeedsEvictionCallback, "should be recycled now");
70+
static ValueTask<Customer> GetAsync() => new(new Customer { Id = 42, Name = "Fred" });
71+
}
72+
73+
private static readonly HybridCacheEntryOptions NoUnderlying = new HybridCacheEntryOptions { Flags = HybridCacheEntryFlags.DisableUnderlyingData };
74+
75+
class TestCache : MemoryDistributedCache, IBufferDistributedCache
76+
{
77+
public TestCache(IOptions<MemoryDistributedCacheOptions> options) : base(options) { }
5978

79+
void IBufferDistributedCache.Set(string key, ReadOnlySequence<byte> value, DistributedCacheEntryOptions options)
80+
=> Set(key, value.ToArray(), options); // efficiency not important for this
81+
82+
ValueTask IBufferDistributedCache.SetAsync(string key, ReadOnlySequence<byte> value, DistributedCacheEntryOptions options, CancellationToken token)
83+
=> new(SetAsync(key, value.ToArray(), options, token)); // efficiency not important for this
84+
85+
bool IBufferDistributedCache.TryGet(string key, IBufferWriter<byte> destination)
86+
=> Write(destination, Get(key));
87+
88+
async ValueTask<bool> IBufferDistributedCache.TryGetAsync(string key, IBufferWriter<byte> destination, CancellationToken token)
89+
=> Write(destination, await GetAsync(key, token));
90+
91+
static bool Write(IBufferWriter<byte> destination, byte[]? buffer)
92+
{
93+
if (buffer is null)
94+
{
95+
return false;
96+
}
97+
destination.Write(buffer);
98+
return true;
99+
}
100+
}
101+
102+
[Fact]
103+
public async Task BufferDoesNotNeedRelease_LegacyL2() // byte[] API; not pooled
104+
{
105+
using var provider = GetDefaultCache(out var cache,
106+
services => services.AddSingleton<IDistributedCache, TestCache>());
107+
108+
cache.DebugRemoveFeatures(CacheFeatures.BackendBuffers);
109+
// prep the backend with our data
110+
var key = Me();
111+
Assert.NotNull(cache.BackendCache);
112+
var serializer = cache.GetSerializer<Customer>();
113+
using (var writer = RecyclableArrayBufferWriter<byte>.Create(int.MaxValue))
114+
{
115+
serializer.Serialize(await GetAsync(), writer);
116+
cache.BackendCache.Set(key, writer.ToArray());
117+
}
118+
#if DEBUG
119+
cache.DebugGetOutstandingBuffers(flush: true);
120+
Assert.Equal(0, cache.DebugGetOutstandingBuffers());
121+
#endif
122+
var first = await cache.GetOrCreateAsync(key, _ => GetAsync(), NoUnderlying); // we expect this to come from L2, hence NoUnderlying
123+
Assert.NotNull(first);
124+
#if DEBUG
125+
Assert.Equal(0, cache.DebugGetOutstandingBuffers());
126+
#endif
127+
Assert.True(cache.DebugTryGetCacheItem(key, out var cacheItem));
128+
129+
// assert that we can reserve the buffer *now* (mostly to see that it behaves differently later)
130+
Assert.False(cacheItem.NeedsEvictionCallback, "should NOT be pooled memory");
131+
Assert.True(cacheItem.TryReserveBuffer(out _));
132+
cacheItem.Release(); // for the above reserve
133+
134+
var second = await cache.GetOrCreateAsync(key, _ => GetAsync(), NoUnderlying);
135+
Assert.NotNull(second);
136+
Assert.NotSame(first, second);
137+
138+
await cache.RemoveKeyAsync(key);
139+
var third = await cache.GetOrCreateAsync(key, _ => GetAsync(), NoUnderlying);
140+
Assert.Null(third);
141+
Assert.Null(await cache.BackendCache.GetAsync(key)); // should be gone from L2 too
142+
143+
// give it a moment for the eviction callback to kick in
144+
for (int i = 0; i < 10 && cacheItem.NeedsEvictionCallback; i++)
145+
{
146+
await Task.Delay(250);
147+
}
148+
#if DEBUG
149+
Assert.Equal(0, cache.DebugGetOutstandingBuffers());
150+
#endif
151+
// assert that we can *no longer* reserve this buffer, because we've already recycled it
152+
Assert.True(cacheItem.TryReserveBuffer(out _)); // always readable
153+
cacheItem.Release();
154+
155+
Assert.False(cacheItem.NeedsEvictionCallback, "should still not need recycling");
156+
static ValueTask<Customer> GetAsync() => new(new Customer { Id = 42, Name = "Fred" });
157+
}
158+
159+
[Fact]
160+
public async Task BufferGetsReleased_BufferL2() // IBufferWriter<byte> API; pooled
161+
{
162+
using var provider = GetDefaultCache(out var cache,
163+
services => services.AddSingleton<IDistributedCache, TestCache>());
164+
165+
// prep the backend with our data
166+
var key = Me();
167+
Assert.NotNull(cache.BackendCache);
168+
var serializer = cache.GetSerializer<Customer>();
169+
using (var writer = RecyclableArrayBufferWriter<byte>.Create(int.MaxValue))
170+
{
171+
serializer.Serialize(await GetAsync(), writer);
172+
cache.BackendCache.Set(key, writer.ToArray());
173+
}
174+
#if DEBUG
175+
cache.DebugGetOutstandingBuffers(flush: true);
176+
Assert.Equal(0, cache.DebugGetOutstandingBuffers());
177+
#endif
178+
var first = await cache.GetOrCreateAsync(key, _ => GetAsync(), NoUnderlying); // we expect this to come from L2, hence NoUnderlying
179+
Assert.NotNull(first);
180+
#if DEBUG
181+
Assert.Equal(1, cache.DebugGetOutstandingBuffers());
182+
#endif
183+
Assert.True(cache.DebugTryGetCacheItem(key, out var cacheItem));
184+
185+
// assert that we can reserve the buffer *now* (mostly to see that it behaves differently later)
186+
Assert.True(cacheItem.NeedsEvictionCallback, "should be pooled memory");
187+
Assert.True(cacheItem.TryReserveBuffer(out _));
188+
cacheItem.Release(); // for the above reserve
189+
190+
var second = await cache.GetOrCreateAsync(key, _ => GetAsync(), NoUnderlying);
191+
Assert.NotNull(second);
192+
Assert.NotSame(first, second);
193+
194+
await cache.RemoveKeyAsync(key);
195+
var third = await cache.GetOrCreateAsync(key, _ => GetAsync(), NoUnderlying);
196+
Assert.Null(third);
197+
Assert.Null(await cache.BackendCache.GetAsync(key)); // should be gone from L2 too
198+
199+
// give it a moment for the eviction callback to kick in
200+
for (int i = 0; i < 10 && cacheItem.NeedsEvictionCallback; i++)
201+
{
202+
await Task.Delay(250);
203+
}
204+
#if DEBUG
205+
Assert.Equal(0, cache.DebugGetOutstandingBuffers());
206+
#endif
207+
// assert that we can *no longer* reserve this buffer, because we've already recycled it
208+
Assert.False(cacheItem.TryReserveBuffer(out _)); // released now
209+
210+
Assert.False(cacheItem.NeedsEvictionCallback, "should be recycled by now");
60211
static ValueTask<Customer> GetAsync() => new(new Customer { Id = 42, Name = "Fred" });
61212
}
62213

214+
63215
public class Customer
64216
{
65217
public int Id { get; set; }

0 commit comments

Comments
 (0)