1
1
using System . Collections . Concurrent ;
2
2
using System . Diagnostics . CodeAnalysis ;
3
3
using System . Linq . Dynamic . Core . Validation ;
4
+ using System . Threading ;
4
5
5
6
namespace System . Linq . Dynamic . Core . Util . Cache ;
6
7
@@ -11,7 +12,9 @@ internal class SlidingCache<TKey, TValue> where TKey : notnull where TValue : no
11
12
private readonly IDateTimeUtils _dateTimeProvider ;
12
13
private readonly Action _deleteExpiredCachedItemsDelegate ;
13
14
private readonly long ? _minCacheItemsBeforeCleanup ;
15
+ private readonly bool _returnExpiredItems ;
14
16
private DateTime _lastCleanupTime ;
17
+ private int _cleanupLocked = 0 ;
15
18
16
19
/// <summary>
17
20
/// Sliding Thread Safe Cache
@@ -26,15 +29,19 @@ internal class SlidingCache<TKey, TValue> where TKey : notnull where TValue : no
26
29
/// Provides the Time for the Caching object. Default will be created if not supplied. Used
27
30
/// for Testing classes
28
31
/// </param>
32
+ /// <param name="returnExpiredItems">If a request for an item happens to be expired, but is still
33
+ /// in known, don't expire it and return it instead.</param>
29
34
public SlidingCache (
30
35
TimeSpan timeToLive ,
31
36
TimeSpan ? cleanupFrequency = null ,
32
37
long ? minCacheItemsBeforeCleanup = null ,
33
- IDateTimeUtils ? dateTimeProvider = null )
38
+ IDateTimeUtils ? dateTimeProvider = null ,
39
+ bool returnExpiredItems = false )
34
40
{
35
41
_cache = new ConcurrentDictionary < TKey , CacheEntry < TValue > > ( ) ;
36
42
TimeToLive = timeToLive ;
37
43
_minCacheItemsBeforeCleanup = minCacheItemsBeforeCleanup ;
44
+ _returnExpiredItems = returnExpiredItems ;
38
45
_cleanupFrequency = cleanupFrequency ?? SlidingCacheConstants . DefaultCleanupFrequency ;
39
46
_deleteExpiredCachedItemsDelegate = Cleanup ;
40
47
_dateTimeProvider = dateTimeProvider ?? new DateTimeUtils ( ) ;
@@ -58,6 +65,7 @@ public SlidingCache(CacheConfig cacheConfig, IDateTimeUtils? dateTimeProvider =
58
65
TimeToLive = cacheConfig . TimeToLive ;
59
66
_minCacheItemsBeforeCleanup = cacheConfig . MinItemsTrigger ;
60
67
_cleanupFrequency = cacheConfig . CleanupFrequency ;
68
+ _returnExpiredItems = cacheConfig . ReturnExpiredItems ;
61
69
_deleteExpiredCachedItemsDelegate = Cleanup ;
62
70
_dateTimeProvider = dateTimeProvider ?? new DateTimeUtils ( ) ;
63
71
// To prevent a scan on first call, set the last Cleanup to the current Provider time
@@ -100,20 +108,29 @@ public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value)
100
108
{
101
109
Check . NotNull ( key ) ;
102
110
103
- CleanupIfNeeded ( ) ;
104
-
105
- if ( _cache . TryGetValue ( key , out var valueAndExpiration ) )
111
+ try
106
112
{
107
- if ( _dateTimeProvider . UtcNow <= valueAndExpiration . ExpirationTime )
113
+ if ( _cache . TryGetValue ( key , out var valueAndExpiration ) )
108
114
{
109
- value = valueAndExpiration . Value ;
110
- var newExpire = _dateTimeProvider . UtcNow . Add ( TimeToLive ) ;
111
- _cache [ key ] = new CacheEntry < TValue > ( value , newExpire ) ;
112
- return true ;
115
+ // Permit expired returns will return the object even if was expired
116
+ // this will prevent the need to re-create the object for the caller
117
+ if ( _returnExpiredItems || _dateTimeProvider . UtcNow <= valueAndExpiration . ExpirationTime )
118
+ {
119
+ value = valueAndExpiration . Value ;
120
+ var newExpire = _dateTimeProvider . UtcNow . Add ( TimeToLive ) ;
121
+ _cache [ key ] = new CacheEntry < TValue > ( value , newExpire ) ;
122
+ return true ;
123
+ }
124
+
125
+ // Remove expired item
126
+ _cache . TryRemove ( key , out _ ) ;
113
127
}
114
-
115
- // Remove expired item
116
- _cache . TryRemove ( key , out _ ) ;
128
+ }
129
+ finally
130
+ {
131
+ // If permit expired returns are enabled,
132
+ // we want to ensure the cache has a chance to get the value
133
+ CleanupIfNeeded ( ) ;
117
134
}
118
135
119
136
value = default ;
@@ -131,26 +148,41 @@ public bool Remove(TKey key)
131
148
132
149
private void CleanupIfNeeded ( )
133
150
{
134
- if ( _dateTimeProvider . UtcNow - _lastCleanupTime > _cleanupFrequency
135
- && ( _minCacheItemsBeforeCleanup == null ||
136
- _cache . Count >=
137
- _minCacheItemsBeforeCleanup ) // Only cleanup if we have a minimum number of items in the cache.
138
- )
151
+ // Ensure this is only executing one at a time.
152
+ if ( Interlocked . CompareExchange ( ref _cleanupLocked , 1 , 0 ) != 0 )
153
+ {
154
+ return ;
155
+ }
156
+
157
+ try
139
158
{
140
- // Set here, so we don't have re-entry due to large collection enumeration.
141
- _lastCleanupTime = _dateTimeProvider . UtcNow ;
159
+ if ( _dateTimeProvider . UtcNow - _lastCleanupTime > _cleanupFrequency
160
+ && ( _minCacheItemsBeforeCleanup == null ||
161
+ _cache . Count >=
162
+ _minCacheItemsBeforeCleanup ) // Only cleanup if we have a minimum number of items in the cache.
163
+ )
164
+ {
165
+ // Set here, so we don't have re-entry due to large collection enumeration.
166
+ _lastCleanupTime = _dateTimeProvider . UtcNow ;
142
167
143
- TaskUtils . Run ( _deleteExpiredCachedItemsDelegate ) ;
168
+ TaskUtils . Run ( _deleteExpiredCachedItemsDelegate ) ;
169
+ }
170
+ }
171
+ finally
172
+ {
173
+ // Release the lock
174
+ _cleanupLocked = 0 ;
144
175
}
145
176
}
146
177
147
178
private void Cleanup ( )
148
179
{
149
- foreach ( var key in _cache . Keys )
180
+ // Enumerate the key/value - safe per docs
181
+ foreach ( var keyValue in _cache )
150
182
{
151
- if ( _dateTimeProvider . UtcNow > _cache [ key ] . ExpirationTime )
183
+ if ( _dateTimeProvider . UtcNow > keyValue . Value . ExpirationTime )
152
184
{
153
- _cache . TryRemove ( key , out _ ) ;
185
+ _cache . TryRemove ( keyValue . Key , out _ ) ;
154
186
}
155
187
}
156
188
}
0 commit comments