Skip to content

Commit 223f1bd

Browse files
committed
avoid deserialize inside lock, involves refactoring cache item so we can reserve against a cache-item
1 parent 7f8fbc6 commit 223f1bd

13 files changed

+134
-137
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/DefaultHybridCache.CacheItem.cs

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

44
using System;
5+
using System.Threading;
56
using Microsoft.Extensions.Caching.Memory;
67

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

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

26-
public virtual void OnEviction() { } // only invoked if NeedsEvictionCallback reported true
35+
protected virtual void OnFinalRelease() { } // any required release semantics
2736

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

3039
public abstract bool DebugIsImmutable { get; }
40+
41+
public bool Release() // returns true ONLY for the final release step
42+
{
43+
var newCount = Interlocked.Decrement(ref _refCount);
44+
if (newCount == 0)
45+
{
46+
// perform per-item clean-up, i.e. buffer recycling (if defensive copies needed)
47+
OnFinalRelease();
48+
return true;
49+
}
50+
return false;
51+
}
52+
53+
public bool TryReserve()
54+
{
55+
var oldValue = Volatile.Read(ref _refCount);
56+
do
57+
{
58+
if (oldValue is 0 or -1)
59+
{
60+
return false; // already burned, or about to roll around back to zero
61+
}
62+
63+
var updated = Interlocked.CompareExchange(ref _refCount, oldValue + 1, oldValue);
64+
if (updated == oldValue)
65+
{
66+
return true; // we exchanged
67+
}
68+
oldValue = updated; // we failed, but we have an updated state
69+
} while (true);
70+
}
71+
3172
}
3273

3374
internal abstract class CacheItem<T> : CacheItem
3475
{
76+
// attempt to get a value that was *not* previously reserved
3577
public abstract bool TryGetValue(out T value);
3678

37-
public T GetValue()
79+
// get a value that *was* reserved, countermanding our reservation in the process
80+
public T GetReservedValue()
3881
{
3982
if (!TryGetValue(out var value))
4083
{
4184
Throw();
4285
}
86+
Release();
4387
return value;
4488

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

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ partial class DefaultHybridCache
1111
{
1212
private sealed class ImmutableCacheItem<T> : CacheItem<T> // used to hold types that do not require defensive copies
1313
{
14-
private readonly T _value;
15-
public ImmutableCacheItem(T value) => _value = value;
14+
private T _value = default!; // deferred until SetValue
15+
16+
public void SetValue(T value) => _value = value;
1617

1718
private static ImmutableCacheItem<T>? _sharedDefault;
1819

1920
// 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+
public static ImmutableCacheItem<T> Default => _sharedDefault ??= new();
2122

2223
public override bool TryGetValue(out T value)
2324
{

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-
DebugOnlyDecrementOutstandingBuffers();
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)

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,9 @@ public bool GetOrCreateStampedeState<TState, T>(string key, HybridCacheEntryFlag
6161
// and for *them* to have updated needs local-cache-write, but since the shared us/them key includes flags,
6262
// we can skip this if *either* flag is set)
6363
if ((flags & HybridCacheEntryFlags.DisableLocalCache) == 0 && _localCache.TryGetValue(key, out var untyped)
64-
&& untyped is CacheItem<T> typed && typed.TryGetValue(out var value))
64+
&& untyped is CacheItem<T> typed && typed.TryReserve())
6565
{
66-
// set the value against the *current* stampede-state, essentially making it pre-completed; we
67-
// can emulate that by using ImmutableCacheItem (if it isn't already), noting that this state
68-
// will never be in the dictionary, so this value is never shared with anyone else
69-
if (typed is not ImmutableCacheItem<T> immutable)
70-
{
71-
immutable = new ImmutableCacheItem<T>(value);
72-
}
73-
stampedeState.SetResultDirect(immutable);
66+
stampedeState.SetResultDirect(typed);
7467
return false; // the work has ALREADY been done
7568
}
7669

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

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ internal abstract class StampedeState
1515
#endif
1616
{
1717
private readonly DefaultHybridCache _cache;
18-
private int _activeCallers = 1;
18+
private readonly CacheItem _cacheItem;
1919

2020
// because multiple callers can enlist, we need to track when the *last* caller cancels
2121
// (and keep going until then); that means we need to run with custom cancellation
@@ -26,14 +26,16 @@ internal abstract class StampedeState
2626
// (both in terms of width and copy-semantics)
2727
private readonly StampedeKey _key;
2828
public ref readonly StampedeKey Key => ref _key;
29+
protected CacheItem CacheItem => _cacheItem;
2930

3031
/// <summary>
3132
/// Create a stamped token optionally with shared cancellation support
3233
/// </summary>
33-
protected StampedeState(DefaultHybridCache cache, in StampedeKey key, bool canBeCanceled)
34+
protected StampedeState(DefaultHybridCache cache, in StampedeKey key, CacheItem cacheItem, bool canBeCanceled)
3435
{
3536
_cache = cache;
3637
_key = key;
38+
_cacheItem = cacheItem;
3739
if (canBeCanceled)
3840
{
3941
// if the first (or any) caller can't be cancelled; we'll never get to zero; no point tracking
@@ -50,10 +52,11 @@ protected StampedeState(DefaultHybridCache cache, in StampedeKey key, bool canBe
5052
/// <summary>
5153
/// Create a stamped token using a fixed cancellation token
5254
/// </summary>
53-
protected StampedeState(DefaultHybridCache cache, in StampedeKey key, CancellationToken token)
55+
protected StampedeState(DefaultHybridCache cache, in StampedeKey key, CacheItem cacheItem, CancellationToken token)
5456
{
5557
_cache = cache;
5658
_key = key;
59+
_cacheItem = cacheItem;
5760
SharedToken = token;
5861
}
5962

@@ -71,39 +74,22 @@ protected StampedeState(DefaultHybridCache cache, in StampedeKey key, Cancellati
7174

7275
public abstract void SetCanceled();
7376

74-
public int DebugCallerCount => Volatile.Read(ref _activeCallers);
77+
public int DebugCallerCount => _cacheItem.RefCount;
7578

7679
public abstract Type Type { get; }
7780

7881
public void RemoveCaller()
7982
{
8083
// note that TryAddCaller has protections to avoid getting back from zero
81-
if (Interlocked.Decrement(ref _activeCallers) == 0)
84+
if (_cacheItem.Release())
8285
{
8386
// we're the last to leave; turn off the lights
8487
_sharedCancellation?.Cancel();
8588
SetCanceled();
8689
}
8790
}
8891

89-
public bool TryAddCaller() // essentially just interlocked-increment, but with a leading zero check and overflow detection
90-
{
91-
var oldValue = Volatile.Read(ref _activeCallers);
92-
do
93-
{
94-
if (oldValue is 0 or -1)
95-
{
96-
return false; // already burned or about to roll around back to zero
97-
}
98-
99-
var updated = Interlocked.CompareExchange(ref _activeCallers, oldValue + 1, oldValue);
100-
if (updated == oldValue)
101-
{
102-
return true; // we exchanged
103-
}
104-
oldValue = updated; // we failed, but we have an updated state
105-
} while (true);
106-
}
92+
public bool TryAddCaller() => _cacheItem.TryReserve();
10793
}
10894

10995
private void RemoveStampedeState(in StampedeKey key)

0 commit comments

Comments
 (0)