66using System . Threading ;
77using System . Threading . Tasks ;
88using CacheTower . Extensions ;
9+ using CacheTower . Providers . Memory ;
910using Microsoft . Extensions . DependencyInjection ;
1011
1112namespace CacheTower
@@ -89,17 +90,17 @@ public async ValueTask EvictAsync(string cacheKey)
8990 }
9091 }
9192
92- public async ValueTask < CacheEntry < T > > SetAsync < T > ( string cacheKey , T value , TimeSpan timeToLive )
93+ public async ValueTask < CacheEntry < T > > SetAsync < T > ( string cacheKey , T value , TimeSpan timeToLive , CacheSettings settings = default )
9394 {
9495 ThrowIfDisposed ( ) ;
9596
9697 var expiry = DateTime . UtcNow + timeToLive ;
9798 var cacheEntry = new CacheEntry < T > ( value , expiry ) ;
98- await SetAsync ( cacheKey , cacheEntry ) ;
99+ await SetAsync ( cacheKey , cacheEntry , settings ) ;
99100 return cacheEntry ;
100101 }
101102
102- public async ValueTask SetAsync < T > ( string cacheKey , CacheEntry < T > cacheEntry )
103+ public async ValueTask SetAsync < T > ( string cacheKey , CacheEntry < T > cacheEntry , CacheSettings settings = default )
103104 {
104105 ThrowIfDisposed ( ) ;
105106
@@ -113,16 +114,23 @@ public async ValueTask SetAsync<T>(string cacheKey, CacheEntry<T> cacheEntry)
113114 throw new ArgumentNullException ( nameof ( cacheEntry ) ) ;
114115 }
115116
116- for ( int i = 0 , l = CacheLayers . Length ; i < l ; i ++ )
117+ if ( settings . ForwardPropagateAfterXCacheHits > 0 && CacheLayers [ 0 ] is MemoryCacheLayer memoryCacheLayer )
117118 {
118- var layer = CacheLayers [ i ] ;
119- if ( layer is ISyncCacheLayer syncLayerOne )
120- {
121- syncLayerOne . Set ( cacheKey , cacheEntry ) ;
122- }
123- else
119+ memoryCacheLayer . Set ( cacheKey , cacheEntry ) ;
120+ }
121+ else
122+ {
123+ for ( int i = 0 , l = CacheLayers . Length ; i < l ; i ++ )
124124 {
125- await ( layer as IAsyncCacheLayer ) . SetAsync ( cacheKey , cacheEntry ) ;
125+ var layer = CacheLayers [ i ] ;
126+ if ( layer is ISyncCacheLayer syncLayerOne )
127+ {
128+ syncLayerOne . Set ( cacheKey , cacheEntry ) ;
129+ }
130+ else
131+ {
132+ await ( layer as IAsyncCacheLayer ) . SetAsync ( cacheKey , cacheEntry ) ;
133+ }
126134 }
127135 }
128136 }
@@ -201,7 +209,7 @@ public async ValueTask<CacheEntry<T>> GetAsync<T>(string cacheKey)
201209 return default ;
202210 }
203211
204- public async ValueTask < T > GetOrSetAsync < T > ( string cacheKey , Func < T , Task < T > > getter , CacheSettings settings )
212+ public async ValueTask < T > GetOrSetAsync < T > ( string cacheKey , Func < T , Task < T > > getter , CacheEntryLifetime entryLifetime , CacheSettings settings = default )
205213 {
206214 ThrowIfDisposed ( ) ;
207215
@@ -220,13 +228,14 @@ public async ValueTask<T> GetOrSetAsync<T>(string cacheKey, Func<T, Task<T>> get
220228 {
221229 var cacheEntry = cacheEntryPoint . CacheEntry ;
222230 var currentTime = DateTime . UtcNow ;
223- if ( cacheEntry . GetStaleDate ( settings ) < currentTime )
231+ var isStale = cacheEntry . GetStaleDate ( entryLifetime ) < currentTime ;
232+ if ( isStale )
224233 {
225234 if ( cacheEntry . Expiry < currentTime )
226235 {
227236 //Refresh the value in the current thread though short circuit if we're unable to establish a lock
228237 //If the lock isn't established, it will instead use the stale cache entry (even if past the allowed stale period)
229- var refreshedCacheEntry = await RefreshValueAsync ( cacheKey , getter , settings , waitForRefresh : false ) ;
238+ var refreshedCacheEntry = await RefreshValueAsync ( cacheKey , getter , entryLifetime , settings , waitForRefresh : false ) ;
230239 if ( refreshedCacheEntry != default )
231240 {
232241 cacheEntry = refreshedCacheEntry ;
@@ -235,62 +244,56 @@ public async ValueTask<T> GetOrSetAsync<T>(string cacheKey, Func<T, Task<T>> get
235244 else
236245 {
237246 //Refresh the value in the background
238- _ = RefreshValueAsync ( cacheKey , getter , settings , waitForRefresh : false ) ;
247+ _ = RefreshValueAsync ( cacheKey , getter , entryLifetime , settings , waitForRefresh : false ) ;
239248 }
240249 }
241- else if ( cacheEntryPoint . LayerIndex > 0 )
250+ else
242251 {
243- //If a lower-level cache is missing the latest data, attempt to set it in the background
244- _ = BackPopulateCacheAsync ( cacheEntryPoint . LayerIndex , cacheKey , cacheEntry ) ;
252+ if ( cacheEntryPoint . LayerIndex > 0 )
253+ {
254+ //If a lower-level cache (eg. a memory cache) is missing the latest data, attempt to set it in the background
255+ _ = BackPropagateCacheEntryAsync ( cacheEntryPoint . LayerIndex , cacheKey , cacheEntry ) ;
256+ }
257+ else if ( ! cacheEntry . _HasBeenForwardPropagated && settings . ForwardPropagateAfterXCacheHits > 0 && cacheEntry . CacheHitCount >= settings . ForwardPropagateAfterXCacheHits )
258+ {
259+ //If enabled, we push the local cache entry to higher-level caches, doing so in the background
260+ _ = ForwardPropagateCacheEntryAsync ( cacheEntryPoint . LayerIndex + 1 , cacheKey , cacheEntry ) ;
261+ }
245262 }
246263
247264 return cacheEntry . Value ;
248265 }
249266 else
250267 {
251268 //Refresh the value in the current thread though because we have no old cache value, we have to lock and wait
252- return ( await RefreshValueAsync ( cacheKey , getter , settings , waitForRefresh : true ) ) . Value ;
269+ return ( await RefreshValueAsync ( cacheKey , getter , entryLifetime , settings , waitForRefresh : true ) ) . Value ;
253270 }
254271 }
255272
256- private async ValueTask BackPopulateCacheAsync < T > ( int fromIndexExclusive , string cacheKey , CacheEntry < T > cacheEntry )
273+ private async ValueTask BackPropagateCacheEntryAsync < T > ( int fromIndexExclusive , string cacheKey , CacheEntry < T > cacheEntry )
257274 {
258275 ThrowIfDisposed ( ) ;
259276
260- var hasLock = false ;
261- lock ( WaitingKeyRefresh )
262- {
263- #if NETSTANDARD2_0
264- hasLock = ! WaitingKeyRefresh . ContainsKey ( cacheKey ) ;
265- if ( hasLock )
266- {
267- WaitingKeyRefresh [ cacheKey ] = Array . Empty < TaskCompletionSource < object > > ( ) ;
268- }
269- #elif NETSTANDARD2_1
270- hasLock = WaitingKeyRefresh . TryAdd ( cacheKey , Array . Empty < TaskCompletionSource < object > > ( ) ) ;
271- #endif
272- }
273-
274- if ( hasLock )
277+ if ( TryGetKeyRefreshLock ( cacheKey ) )
275278 {
276279 try
277280 {
278281 for ( ; -- fromIndexExclusive >= 0 ; )
279282 {
280- var previousLayer = CacheLayers [ fromIndexExclusive ] ;
281- if ( previousLayer is ISyncCacheLayer prevSyncLayer )
283+ var cacheLayer = CacheLayers [ fromIndexExclusive ] ;
284+ if ( cacheLayer is ISyncCacheLayer syncLayer )
282285 {
283- if ( prevSyncLayer . IsAvailable ( cacheKey ) )
286+ if ( syncLayer . IsAvailable ( cacheKey ) )
284287 {
285- prevSyncLayer . Set ( cacheKey , cacheEntry ) ;
288+ syncLayer . Set ( cacheKey , cacheEntry ) ;
286289 }
287290 }
288291 else
289292 {
290- var prevAsyncLayer = previousLayer as IAsyncCacheLayer ;
291- if ( await prevAsyncLayer . IsAvailableAsync ( cacheKey ) )
293+ var asyncCacheLayer = cacheLayer as IAsyncCacheLayer ;
294+ if ( await asyncCacheLayer . IsAvailableAsync ( cacheKey ) )
292295 {
293- await prevAsyncLayer . SetAsync ( cacheKey , cacheEntry ) ;
296+ await asyncCacheLayer . SetAsync ( cacheKey , cacheEntry ) ;
294297 }
295298 }
296299 }
@@ -302,25 +305,48 @@ private async ValueTask BackPopulateCacheAsync<T>(int fromIndexExclusive, string
302305 }
303306 }
304307
305- private async ValueTask < CacheEntry < T > > RefreshValueAsync < T > ( string cacheKey , Func < T , Task < T > > getter , CacheSettings settings , bool waitForRefresh )
308+ private async ValueTask ForwardPropagateCacheEntryAsync < T > ( int fromIndexExclusive , string cacheKey , CacheEntry < T > cacheEntry )
306309 {
307310 ThrowIfDisposed ( ) ;
308311
309- var hasLock = false ;
310- lock ( WaitingKeyRefresh )
312+ if ( TryGetKeyRefreshLock ( cacheKey ) && cacheEntry . _HasBeenForwardPropagated )
311313 {
312- #if NETSTANDARD2_0
313- hasLock = ! WaitingKeyRefresh . ContainsKey ( cacheKey ) ;
314- if ( hasLock )
314+ try
315315 {
316- WaitingKeyRefresh [ cacheKey ] = Array . Empty < TaskCompletionSource < object > > ( ) ;
316+ for ( ; ++ fromIndexExclusive < CacheLayers . Length ; )
317+ {
318+ var cacheLayer = CacheLayers [ fromIndexExclusive ] ;
319+ if ( cacheLayer is ISyncCacheLayer syncLayer )
320+ {
321+ if ( syncLayer . IsAvailable ( cacheKey ) )
322+ {
323+ syncLayer . Set ( cacheKey , cacheEntry ) ;
324+ }
325+ }
326+ else
327+ {
328+ var asyncCacheLayer = cacheLayer as IAsyncCacheLayer ;
329+ if ( await asyncCacheLayer . IsAvailableAsync ( cacheKey ) )
330+ {
331+ await asyncCacheLayer . SetAsync ( cacheKey , cacheEntry ) ;
332+ }
333+ }
334+ }
335+
336+ cacheEntry . _HasBeenForwardPropagated = true ;
337+ }
338+ finally
339+ {
340+ UnlockWaitingTasks ( cacheKey , cacheEntry ) ;
317341 }
318- #elif NETSTANDARD2_1
319- hasLock = WaitingKeyRefresh . TryAdd ( cacheKey , Array . Empty < TaskCompletionSource < object > > ( ) ) ;
320- #endif
321342 }
343+ }
322344
323- if ( hasLock )
345+ private async ValueTask < CacheEntry < T > > RefreshValueAsync < T > ( string cacheKey , Func < T , Task < T > > getter , CacheEntryLifetime entryLifetime , CacheSettings settings , bool waitForRefresh )
346+ {
347+ ThrowIfDisposed ( ) ;
348+
349+ if ( TryGetKeyRefreshLock ( cacheKey ) )
324350 {
325351 try
326352 {
@@ -335,14 +361,14 @@ private async ValueTask<CacheEntry<T>> RefreshValueAsync<T>(string cacheKey, Fun
335361 }
336362
337363 var value = await getter ( oldValue ) ;
338- var refreshedEntry = await SetAsync ( cacheKey , value , settings . TimeToLive ) ;
364+ var refreshedEntry = await SetAsync ( cacheKey , value , entryLifetime . TimeToLive , settings ) ;
339365
340- _ = Extensions . OnValueRefreshAsync ( cacheKey , settings . TimeToLive ) ;
366+ _ = Extensions . OnValueRefreshAsync ( cacheKey , entryLifetime . TimeToLive ) ;
341367
342368 UnlockWaitingTasks ( cacheKey , refreshedEntry ) ;
343369
344370 return refreshedEntry ;
345- } , settings ) ;
371+ } , entryLifetime ) ;
346372 }
347373 catch
348374 {
@@ -369,7 +395,7 @@ private async ValueTask<CacheEntry<T>> RefreshValueAsync<T>(string cacheKey, Fun
369395
370396 //Last minute check to confirm whether waiting is required
371397 var currentEntry = await GetAsync < T > ( cacheKey ) ;
372- if ( currentEntry != null && currentEntry . GetStaleDate ( settings ) > DateTime . UtcNow )
398+ if ( currentEntry != null && currentEntry . GetStaleDate ( entryLifetime ) > DateTime . UtcNow )
373399 {
374400 UnlockWaitingTasks ( cacheKey , currentEntry ) ;
375401 return currentEntry ;
@@ -400,6 +426,25 @@ private void UnlockWaitingTasks(string cacheKey, CacheEntry cacheEntry)
400426 }
401427 }
402428
429+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
430+ private bool TryGetKeyRefreshLock ( string cacheKey )
431+ {
432+ var hasLock = false ;
433+ lock ( WaitingKeyRefresh )
434+ {
435+ #if NETSTANDARD2_0
436+ hasLock = ! WaitingKeyRefresh . ContainsKey ( cacheKey ) ;
437+ if ( hasLock )
438+ {
439+ WaitingKeyRefresh [ cacheKey ] = Array . Empty < TaskCompletionSource < object > > ( ) ;
440+ }
441+ #elif NETSTANDARD2_1
442+ hasLock = WaitingKeyRefresh . TryAdd ( cacheKey , Array . Empty < TaskCompletionSource < object > > ( ) ) ;
443+ #endif
444+ }
445+ return hasLock ;
446+ }
447+
403448#if NETSTANDARD2_0
404449 public void Dispose ( )
405450 {
0 commit comments