// This method looks up whether there's a request in progress for the given key. If there is, it returns that ongoing request's task. // Otherwise, it triggers a new request by calling the supplied send(), tracks it in RequestsInProgress, and returns that ongoing request task. // Once send() is done, it removes it from the list of RequestsInProgress. private async Task <object> GroupRequestsIfNeeded(IMethodCachingSettings settings, string key, string[] metricsKeys, Func <Task <object> > send) { if (settings.RequestGroupingBehavior == RequestGroupingBehavior.Disabled) { Misses.Mark(metricsKeys); return(await send()); } else { JoinedTeam.Mark(metricsKeys); return(await RequestsInProgress.GetOrAdd(key, _ => new Lazy <Task <object> >(async() => { Misses.Mark(metricsKeys); try { return await send(); } finally { RequestsInProgress.TryRemove(key, out var _); } })).Value); } }
private Task <object> GetOrAdd(string key, Func <Task <object> > factory, CacheItemPolicyEx policy, string[] metricsKeys, Type taskResultType) { Func <bool, Task <object> > wrappedFactory = async removeOnException => { try { var result = await factory().ConfigureAwait(false); //Can happen if item removed before task is completed if (MemoryCache.Contains(key)) { var revocableResult = result as IRevocable; if (revocableResult?.RevokeKeys != null) { foreach (var revokeKey in revocableResult.RevokeKeys) { var cacheKeys = RevokeKeyToCacheKeysIndex.GetOrAdd(revokeKey, k => new HashSet <string>()); lock (cacheKeys) { cacheKeys.Add(key); } } } } AwaitingResult.Decrement(metricsKeys); return(result); } catch { if (removeOnException) { MemoryCache.Remove(key); // Do not cache exceptions. } AwaitingResult.Decrement(metricsKeys); Failed.Mark(metricsKeys); throw; } }; var newItem = new AsyncCacheItem(); Task <object> resultTask; // Taking a lock on the newItem in case it actually becomes the item in the cache (if no item with that key // existed). For another thread, it will be returned into the existingItem variable and will block on the // second lock, preventing concurrent mutation of the same object. lock (newItem.Lock) { if (typeof(IRevocable).IsAssignableFrom(taskResultType)) { policy.RemovedCallback += ItemRemovedCallback; } // Surprisingly, when using MemoryCache.AddOrGetExisting() where the item doesn't exist in the cache, // null is returned. var existingItem = (AsyncCacheItem)MemoryCache.AddOrGetExisting(key, newItem, policy); if (existingItem == null) { Misses.Mark(metricsKeys); AwaitingResult.Increment(metricsKeys); newItem.CurrentValueTask = wrappedFactory(true); newItem.NextRefreshTime = DateTime.UtcNow + policy.RefreshTime; resultTask = newItem.CurrentValueTask; } else { // This lock makes sure we're not mutating the same object as was added to the cache by an earlier // thread (which was the first to add from 'newItem', for subsequent threads it will be 'existingItem'). lock (existingItem.Lock) { resultTask = existingItem.CurrentValueTask; // Start refresh if an existing refresh ins't in progress and we've passed the next refresh time. if (existingItem.RefreshTask == null && DateTime.UtcNow >= existingItem.NextRefreshTime) { existingItem.RefreshTask = ((Func <Task>)(async() => { try { var getNewValue = wrappedFactory(false); await getNewValue.ConfigureAwait(false); existingItem.CurrentValueTask = getNewValue; existingItem.NextRefreshTime = DateTime.UtcNow + policy.RefreshTime; existingItem.RefreshTask = null; MemoryCache.Set(new CacheItem(key, existingItem), policy); } catch { existingItem.NextRefreshTime = DateTime.UtcNow + policy.FailedRefreshDelay; existingItem.RefreshTask = null; } })).Invoke(); } } if (resultTask.GetAwaiter().IsCompleted) { Hits.Mark(metricsKeys); } else { JoinedTeam.Mark(metricsKeys); } } } return(resultTask); }
private Task <object> GetOrAdd(string key, Func <Task <object> > factory, CacheItemPolicyEx policy, string groupName, string logData, string[] metricsKeys, Type taskResultType) { var shouldLog = ShouldLog(groupName); async Task <object> WrappedFactory(bool removeOnException) { try { if (shouldLog) { Log.Info(x => x("Cache item is waiting for value to be resolved", unencryptedTags: new { cacheKey = key, cacheGroup = groupName, cacheData = logData })); } var result = await factory().ConfigureAwait(false); if (shouldLog) { Log.Info(x => x("Cache item value is resolved", unencryptedTags: new { cacheKey = key, cacheGroup = groupName, cacheData = logData, value = GetValueForLogging(result) })); } //Can happen if item removed before task is completed if (MemoryCache.Contains(key)) { var revocableResult = result as IRevocable; if (revocableResult?.RevokeKeys != null) { foreach (var revokeKey in revocableResult.RevokeKeys) { var cacheKeys = RevokeKeyToCacheKeysIndex.GetOrAdd(revokeKey, k => new HashSet <string>()); lock (cacheKeys) { cacheKeys.Add(key); } Log.Info(x => x("RevokeKey added to reverse index", unencryptedTags: new { revokeKey = revokeKey, cacheKey = key, cacheGroup = groupName, cacheData = logData })); } } } AwaitingResult.Decrement(metricsKeys); return(result); } catch (Exception exception) { Log.Info(x => x("Error resolving value for cache item", unencryptedTags: new { cacheKey = key, cacheGroup = groupName, cacheData = logData, removeOnException, errorMessage = exception.Message })); if (removeOnException) { MemoryCache.Remove(key); // Do not cache exceptions. } AwaitingResult.Decrement(metricsKeys); Failed.Mark(metricsKeys); throw; } } var newItem = shouldLog ? new AsyncCacheItem { GroupName = string.Intern(groupName), LogData = logData } : new AsyncCacheItem(); // if log is not needed, then do not cache unnecessary details which will blow up the memory Task <object> resultTask; // Taking a lock on the newItem in case it actually becomes the item in the cache (if no item with that key // existed). For another thread, it will be returned into the existingItem variable and will block on the // second lock, preventing concurrent mutation of the same object. lock (newItem.Lock) { if (typeof(IRevocable).IsAssignableFrom(taskResultType)) { policy.RemovedCallback += ItemRemovedCallback; } // Surprisingly, when using MemoryCache.AddOrGetExisting() where the item doesn't exist in the cache, // null is returned. var existingItem = (AsyncCacheItem)MemoryCache.AddOrGetExisting(key, newItem, policy); if (existingItem == null) { Misses.Mark(metricsKeys); AwaitingResult.Increment(metricsKeys); newItem.CurrentValueTask = WrappedFactory(true); newItem.NextRefreshTime = DateTime.UtcNow + policy.RefreshTime; resultTask = newItem.CurrentValueTask; if (shouldLog) { Log.Info(x => x("Item added to cache", unencryptedTags: new { cacheKey = key, cacheGroup = groupName, cacheData = logData })); } } else { // This lock makes sure we're not mutating the same object as was added to the cache by an earlier // thread (which was the first to add from 'newItem', for subsequent threads it will be 'existingItem'). lock (existingItem.Lock) { resultTask = existingItem.CurrentValueTask; // Start refresh if an existing refresh ins't in progress and we've passed the next refresh time. if (existingItem.RefreshTask?.IsCompleted != false && DateTime.UtcNow >= existingItem.NextRefreshTime) { existingItem.RefreshTask = ((Func <Task>)(async() => { try { var getNewValue = WrappedFactory(false); await getNewValue.ConfigureAwait(false); existingItem.CurrentValueTask = getNewValue; existingItem.NextRefreshTime = DateTime.UtcNow + policy.RefreshTime; MemoryCache.Set(new CacheItem(key, existingItem), policy); } catch { existingItem.NextRefreshTime = DateTime.UtcNow + policy.FailedRefreshDelay; } })).Invoke(); } } if (resultTask.GetAwaiter().IsCompleted) { Hits.Mark(metricsKeys); } else { JoinedTeam.Mark(metricsKeys); } } } return(resultTask); }