public void Set( TKey key, TValue value) { AsyncLazyWithRefreshTask <TValue> updateValue = new AsyncLazyWithRefreshTask <TValue>(value, this.cancellationTokenSource.Token); this.values.AddOrUpdate( key, updateValue, (key, originalValue) => updateValue); }
/// <summary> /// <para> /// Gets value corresponding to <paramref name="key"/>. /// </para> /// <para> /// If another initialization function is already running, new initialization function will not be started. /// The result will be result of currently running initialization function. /// </para> /// <para> /// If previous initialization function is successfully completed it will return the value. It is possible this /// value is stale and will only be updated after the force refresh task is complete. /// </para> /// <para> /// Force refresh is true: /// If the key does not exist: It will create and await the new task /// If the key exists and the current task is still running: It will return the existing task /// If the key exists and the current task is already done: It will start a new task to get the updated values. /// Once the refresh task is complete it will be returned to caller. /// If it is a success the value in the cache will be updated. If the refresh task throws an exception the key will be removed from the cache. /// </para> /// <para> /// If previous initialization function failed - new one will be launched. /// </para> /// </summary> public async Task <TValue> GetAsync( TKey key, Func <Task <TValue> > singleValueInitFunc, bool forceRefresh, Action <TValue, TValue> callBackOnForceRefresh) { if (this.values.TryGetValue(key, out AsyncLazyWithRefreshTask <TValue> initialLazyValue)) { if (!forceRefresh) { return(await initialLazyValue.GetValueAsync()); } return(await initialLazyValue.CreateAndWaitForBackgroundRefreshTaskAsync( singleValueInitFunc, () => this.TryRemove(key), callBackOnForceRefresh)); } // The AsyncLazyWithRefreshTask is lazy and won't create the task until GetValue is called. // It's possible multiple threads will call the GetOrAdd for the same key. The current asyncLazy may // not be used if another thread adds it first. AsyncLazyWithRefreshTask <TValue> asyncLazy = new AsyncLazyWithRefreshTask <TValue>(singleValueInitFunc, this.cancellationTokenSource.Token); AsyncLazyWithRefreshTask <TValue> result = this.values.GetOrAdd( key, asyncLazy); // Another thread async lazy was inserted. Just await on the inserted lazy object. if (!object.ReferenceEquals(asyncLazy, result)) { return(await result.GetValueAsync()); } // This means the current caller async lazy was inserted into the concurrent dictionary. // The task is now awaited on so if an exception occurs it can be removed from // the concurrent dictionary. try { return(await result.GetValueAsync()); } catch (Exception e) { DefaultTrace.TraceError( "AsyncCacheNonBlocking Failed GetAsync with key: {0}, Exception: {1}", key.ToString(), e.ToString()); // Remove the failed task from the dictionary so future requests can send other calls.. this.values.TryRemove(key, out _); throw; } }
public async Task <T> CreateAndWaitForBackgroundRefreshTaskAsync( Func <Task <T> > createRefreshTask, Action callbackOnRefreshFailure, Action <T, T> callBackOnForceRefresh) { this.cancellationToken.ThrowIfCancellationRequested(); // The original task is still being created. Just return the original task. Task <T> valueSnapshot = this.value; if (AsyncLazyWithRefreshTask <T> .IsTaskRunning(valueSnapshot)) { return(await valueSnapshot); } // The above check handles the scenario where this value task is still processing. // It will only get here if the valueSnapshot is completed. This is needed for the // callback to compare the original value to the new value. T originalValue = default; if (valueSnapshot != null) { originalValue = await valueSnapshot; } // Use a local reference to avoid it being updated between the check and the await Task <T> refresh = this.refreshInProgress; if (AsyncLazyWithRefreshTask <T> .IsTaskRunning(refresh)) { T result = await refresh; callBackOnForceRefresh?.Invoke(originalValue, result); return(result); } bool createdTask = false; lock (this.valueLock) { if (AsyncLazyWithRefreshTask <T> .IsTaskRunning(this.refreshInProgress)) { refresh = this.refreshInProgress; } else { createdTask = true; this.refreshInProgress = createRefreshTask(); refresh = this.refreshInProgress; } } // Await outside the lock to prevent lock contention if (!createdTask) { T result = await refresh; callBackOnForceRefresh?.Invoke(originalValue, result); return(result); } // It's possible multiple callers entered the method at the same time. The lock above ensures // only a single one will create the refresh task. If this caller created the task await for the // result and update the value. try { T itemResult = await refresh; lock (this) { this.value = Task.FromResult(itemResult); } callBackOnForceRefresh?.Invoke(originalValue, itemResult); return(itemResult); } catch (Exception e) { // faulted with exception DefaultTrace.TraceError( "AsyncLazyWithRefreshTask Failed with: {0}", e.ToString()); callbackOnRefreshFailure(); throw; } }
/// <summary> /// <para> /// Gets value corresponding to <paramref name="key"/>. /// </para> /// <para> /// If another initialization function is already running, new initialization function will not be started. /// The result will be result of currently running initialization function. /// </para> /// <para> /// If previous initialization function is successfully completed it will return the value. It is possible this /// value is stale and will only be updated after the force refresh task is complete. /// </para> /// <para> /// Force refresh is true: /// If the key does not exist: It will create and await the new task /// If the key exists and the current task is still running: It will return the existing task /// If the key exists and the current task is already done: It will start a new task to get the updated values. /// Once the refresh task is complete it will be returned to caller. /// If it is a success the value in the cache will be updated. If the refresh task throws an exception the key will be removed from the cache. /// </para> /// <para> /// If previous initialization function failed - new one will be launched. /// </para> /// </summary> public async Task <TValue> GetAsync( TKey key, Func <TValue, Task <TValue> > singleValueInitFunc, Func <TValue, bool> forceRefresh) { if (this.values.TryGetValue(key, out AsyncLazyWithRefreshTask <TValue> initialLazyValue)) { try { TValue cachedResult = await initialLazyValue.GetValueAsync(); if (forceRefresh == null || !forceRefresh(cachedResult)) { return(cachedResult); } } catch (Exception e) { // This is needed for scenarios where the initial GetAsync was // called but never awaited. if (initialLazyValue.ShouldRemoveFromCacheThreadSafe()) { bool removed = this.TryRemove(key); DefaultTrace.TraceError( "AsyncCacheNonBlocking Failed GetAsync. key: {0}, tryRemoved: {1}, Exception: {2}", key, removed, e); } throw; } try { return(await initialLazyValue.CreateAndWaitForBackgroundRefreshTaskAsync( createRefreshTask : singleValueInitFunc)); } catch (Exception e) { if (initialLazyValue.ShouldRemoveFromCacheThreadSafe()) { DefaultTrace.TraceError( "AsyncCacheNonBlocking.GetAsync with ForceRefresh Failed. key: {0}, Exception: {1}", key, e); // In some scenarios when a background failure occurs like a 404 // the initial cache value should be removed. if (this.removeFromCacheOnBackgroundRefreshException(e)) { this.TryRemove(key); } } throw; } } // The AsyncLazyWithRefreshTask is lazy and won't create the task until GetValue is called. // It's possible multiple threads will call the GetOrAdd for the same key. The current asyncLazy may // not be used if another thread adds it first. AsyncLazyWithRefreshTask <TValue> asyncLazy = new AsyncLazyWithRefreshTask <TValue>(singleValueInitFunc, this.cancellationTokenSource.Token); AsyncLazyWithRefreshTask <TValue> result = this.values.GetOrAdd( key, asyncLazy); // Another thread async lazy was inserted. Just await on the inserted lazy object. if (!object.ReferenceEquals(asyncLazy, result)) { return(await result.GetValueAsync()); } // This means the current caller async lazy was inserted into the concurrent dictionary. // The task is now awaited on so if an exception occurs it can be removed from // the concurrent dictionary. try { return(await result.GetValueAsync()); } catch (Exception e) { DefaultTrace.TraceError( "AsyncCacheNonBlocking Failed GetAsync with key: {0}, Exception: {1}", key.ToString(), e.ToString()); // Remove the failed task from the dictionary so future requests can send other calls.. this.values.TryRemove(key, out _); throw; } }