private void CacheResponse(string cacheKey, DateTime requestSendTime, Task <object> responseTask, IMethodCachingSettings settings) { var cacheItem = new AsyncCacheItem { NextRefreshTime = DateTime.UtcNow + settings.RefreshTime.Value, Value = responseTask, }; // Register each revoke key in the response with the reverse index, if the response is not an exception and is a Revocable<>. // Starting from now, the IsRevoked property in cacheItem might be set to true by another thread in OnRevoked(). var revokeKeys = ExtarctRevokeKeys(responseTask); if (revokeKeys.Any()) { lock (RevokeKeyToCacheItemsIndex) foreach (var revokeKey in revokeKeys) { RevokeKeyToCacheItemsIndex.GetOrAdd(revokeKey, _ => new HashSet <AsyncCacheItem>()).Add(cacheItem); } } // Check if we got a revoke message from one of the revoke keys in the response while the request was in progress. // Do this AFTER the above, so there's no point in time that we don't monitor cache revokes var recentlyRevokedKey = revokeKeys.FirstOrDefault(rk => RecentRevokesCache.TryGetRecentlyRevokedTime(rk, requestSendTime) != null); if (recentlyRevokedKey != null) { Items.Meter("RevokeRaceCondition", Unit.Items).Mark(); Log.Warn(x => x("Got revoke during request, marking as stale", unencryptedTags: new { revokeKey = recentlyRevokedKey, requestSendTime, cacheKey, revokeTime = RecentRevokesCache.TryGetRecentlyRevokedTime(recentlyRevokedKey, requestSendTime) })); cacheItem.IsRevoked = true; } // Set the MemoryCache policy based on our caching settings. If the response has no revoke keys, no need to receive a callback // event when it's removed from the cache since we don't need to remove it from the reverse index. var cacheItemPolicy = new CacheItemPolicy { }; if (settings.ExpirationBehavior == ExpirationBehavior.ExtendExpirationWhenReadFromCache) { cacheItemPolicy.SlidingExpiration = settings.ExpirationTime.Value; } else { cacheItemPolicy.AbsoluteExpiration = DateTime.UtcNow + settings.ExpirationTime.Value; } if (revokeKeys.Any()) { cacheItemPolicy.RemovedCallback += ItemRemovedCallback; } // Store the response in the main cache. Note that this will trigger a removal of a currently-cached value, if any, // i.e. call ItemRemovedCallback() with Reason=Removed. MemoryCache.Set(new CacheItem(cacheKey, cacheItem), cacheItemPolicy); }
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); }
// This method has one of 5 possible outcomes: // // 1. We got a response from the service (null, not null or an exception) and the caching settings dictate that we should cache it: // We cache it with the default RefreshTime. This extends the ExpirationTime as well. // // 2. The caching settings dictate that the response shouldn't be cached, and should be ignored, and currentValue != null : // We return the currentValue and set its next refresh time to be now + FailedRefreshDelay so we don't "attack" the target service // // 3. The caching settings dictate that the response should not be cached, and should be ignored, and currentValue == null : // We return the response anyway, since we don't have a previously-good value // // 4. The caching settings dictate that the response shouldn't be cached, nor ignored and removed from cache: // We return the response and remove the previously-cached response from the cache // // 5. The caching settings dictate that the response shouldn't be cached, nor ignored and remain in cache: // We return the response private async Task <object> TryFetchNewValue(string cacheKey, Func <Task <object> > serviceMethod, IMethodCachingSettings settings, string[] metricsKeys, CallReason callReason, AsyncCacheItem currentValue = null) { // We use the RecentRevokesCache to keep track of recent revoke messages that arrived, to detect if the response we're about // to receive was revoked while in transit. It will track revoke messages as long as the task is not completed. var requestSendTime = DateTime.UtcNow; var tcs = new TaskCompletionSource <bool>(); RecentRevokesCache.RegisterOutgoingRequest(tcs.Task, requestSendTime); // We capture the response from the service here, including if it was an exception var(response, responseKind) = await CallService(serviceMethod, metricsKeys); string outcome = null; // Outcome #1: Cache the response if (settings.ResponseKindsToCache.HasFlag(responseKind)) { outcome = "cached"; CacheResponse(cacheKey, requestSendTime, response, settings); } else if (settings.ResponseKindsToIgnore.HasFlag(responseKind)) { // Outcome #2: Leave old response cached and return it, and set its refresh time to the (short) FailedRefreshDelay if (currentValue != null) { outcome = "ignored_cachedValueExist"; currentValue.NextRefreshTime = DateTime.UtcNow + settings.FailedRefreshDelay.Value; response = currentValue.Value; } // Outcome #3: We don't have currentValue, so we cant ignore response and we return it else { outcome = "ignored_cachedValueDoesNotExist"; } } else //Dont cache and dont ignore (i.e. return it) { outcome = "notCachedNotIgnored"; // Outcome #4: Do not cache response and return it; remove previously-cached value (if exist) if (settings.NotIgnoredResponseBehavior == NotIgnoredResponseBehavior.RemoveCachedResponse) { if (MemoryCache.Remove(cacheKey) != null) { outcome = "notCachedNotIgnored_cachedValueRemoved"; } } // Outcome #5: Do not cache response and return it; leave old response cached // If old response is not null, set its refresh time to the (short) FailedRefreshDelay else if (currentValue != null) { currentValue.NextRefreshTime = DateTime.UtcNow + settings.FailedRefreshDelay.Value; } } Log.Debug(x => x("Service call", unencryptedTags: new { cacheKey, callReason, responseKind, outcome })); tcs.SetResult(true); // RecentRevokesCache can stop tracking revoke keys return(await response); // Might throw stored exception }