private Task OnRevoke(string revokeKey) { if (revokeKey == null) { throw new ArgumentNullException(); } RecentRevokesCache.RegisterRevokeKey(revokeKey, DateTime.UtcNow); var revokeApplied = false; if (RevokeKeyToCacheItemsIndex.TryGetValue(revokeKey, out var _)) { lock (RevokeKeyToCacheItemsIndex) if (RevokeKeyToCacheItemsIndex.TryGetValue(revokeKey, out var hash)) { foreach (var item in hash) { item.IsRevoked = true; } revokeApplied = true; } } if (revokeApplied) { Revokes.Meter("Succeeded", Unit.Events).Mark(); Log.Debug(x => x("Revoke applied", unencryptedTags: new { revokeKey })); } else { Revokes.Meter("Discarded", Unit.Events).Mark(); } return(Task.CompletedTask); }
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); }
// 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 }