/* * Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Text; using Amazon.Util; using Amazon.Runtime.Internal.Util; using ThirdParty.Json.LitJson; using System.Threading; namespace Amazon.Runtime.Internal.Util { /// /// Interface for a non-generic cache. /// public interface ICache { /// /// Clears the entire cache. /// void Clear(); /// /// Maximum time to keep an item around after its last use. /// TimeSpan MaximumItemLifespan { get; set; } /// /// How often should the cache be cleared of old items. /// TimeSpan CacheClearPeriod { get; set; } /// /// The number of items in the cache. /// int ItemCount { get; } } /// /// Interface for a generic cache. /// /// /// public interface ICache : ICache { /// /// Retrieves a value out of the cache or from the source. /// /// /// /// TValue GetValue(TKey key, Func creator); /// /// Retrieves a value out of the cache or from the source. /// If the item was in the cache, isStaleItem is set to true; /// otherwise, if the item comes from the source, isStaleItem is false. /// /// /// /// /// TValue GetValue(TKey key, Func creator, out bool isStaleItem); /// /// Clears a specific value from the cache if it's there. /// /// void Clear(TKey key); /// /// Returns the keys for all items in the cache. /// /// List Keys { get; } /// /// Executes specified operation, catches exception, clears the cache for /// the given key, retries the operation. /// /// /// /// /// /// /// TOut UseCache(TKey key, Func operation, Action onError, Predicate shouldRetryForException); } /// /// SDK-wide cache. /// Provides access to caches specific to a particular set of credentials /// and target region. /// public static class SdkCache { private static object cacheLock = new object(); private static Cache cache = new Cache(); /// /// Clear all caches /// public static void Clear() { cache.Clear(); } /// /// Clear all caches of a particular type /// /// public static void Clear(object cacheType) { lock (cacheLock) { var keys = cache.Keys; foreach (CacheKey key in keys) { if (AWSSDKUtils.AreEqual(key.CacheType, cacheType)) { var value = cache.GetValue(key, null); value.Clear(); } } } } /// /// Retrieve a cache of a specific type for a client object. /// The client object can be null in cases where a cache does /// not correspond to a specific AWS account or target region. /// /// /// /// /// /// /// public static ICache GetCache( object client, object cacheIdentifier, IEqualityComparer keyComparer) { return GetCache(client as AmazonServiceClient, cacheIdentifier, keyComparer); } /// /// Retrieve a cache of a specific type for a client object. /// The client object can be null in cases where a cache does /// not correspond to a specific AWS account or target region. /// /// /// /// /// /// /// public static ICache GetCache( AmazonServiceClient client, object cacheIdentifier, IEqualityComparer keyComparer) { // If client is null, create an empty key to use. // This supports mock frameworks (where the service is mocked up, // but there is no client to get credentials/region from) and // caches that do not depend on a client (such as a cache // for client-side reflection data). CacheKey key; if (client == null) key = CacheKey.Create(cacheIdentifier); else key = CacheKey.Create(client, cacheIdentifier); ICache value = null; lock (cacheLock) { value = cache.GetValue(key, k => new Cache(keyComparer)); } var typedValue = value as Cache; if (value != null && typedValue == null) { throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Unable to cast cache of type {0} as cache of type {1}", value.GetType().FullName, typeof(Cache).FullName)); } return typedValue; } // Composite cache key consisting of credentials, region, service url, cache type internal class CacheKey { public ImmutableCredentials ImmutableCredentials { get; private set; } public RegionEndpoint RegionEndpoint { get; private set; } public string ServiceUrl { get; private set; } public object CacheType { get; private set; } private CacheKey() { ImmutableCredentials = null; RegionEndpoint = null; ServiceUrl = null; CacheType = null; } public static CacheKey Create(AmazonServiceClient client, object cacheType) { if (client == null) throw new ArgumentNullException("client"); var key = new CacheKey(); var credentials = client.Credentials; key.ImmutableCredentials = credentials == null ? null : credentials.GetCredentials(); key.RegionEndpoint = client.Config.RegionEndpoint; key.ServiceUrl = client.Config.ServiceURL; key.CacheType = cacheType; return key; } public static CacheKey Create(object cacheType) { var key = new CacheKey(); key.CacheType = cacheType; return key; } #region Public overrides public override int GetHashCode() { return Hashing.Hash( ImmutableCredentials, RegionEndpoint, ServiceUrl, CacheType); } public override bool Equals(object obj) { if (object.ReferenceEquals(this, obj)) return true; CacheKey ck = obj as CacheKey; if (ck == null) return false; var allEqual = AWSSDKUtils.AreEqual( new object[] { this.ImmutableCredentials, this.RegionEndpoint, this.ServiceUrl, this.CacheType }, new object[] { ck.ImmutableCredentials, ck.RegionEndpoint, ck.ServiceUrl, ck.CacheType }); return allEqual; } #endregion } } // Implementation of generic ICache interface internal class Cache : ICache { #region Private members private Dictionary> Contents; private readonly object CacheLock = new object(); #endregion #region Constructor public Cache(IEqualityComparer keyComparer = null) { Contents = new Dictionary>(keyComparer); MaximumItemLifespan = DefaultMaximumItemLifespan; CacheClearPeriod = DefaultCacheClearPeriod; } #endregion #region Public members public static TimeSpan DefaultMaximumItemLifespan = TimeSpan.FromHours(6); public static TimeSpan DefaultCacheClearPeriod = TimeSpan.FromHours(1); public DateTime LastCacheClean { get; private set; } #endregion #region ICache implementation public TValue GetValue(TKey key, Func creator) { bool isStaleItem; return GetValueHelper(key, out isStaleItem, creator); } public TValue GetValue(TKey key, Func creator, out bool isStaleItem) { return GetValueHelper(key, out isStaleItem, creator); } public void Clear(TKey key) { lock (CacheLock) { Contents.Remove(key); } } public void Clear() { lock (CacheLock) { Contents.Clear(); LastCacheClean = GetCorrectedLocalTime(); } } public List Keys { get { lock (CacheLock) { return Contents.Keys.ToList(); } } } private TimeSpan maximumItemLifespan; public TimeSpan MaximumItemLifespan { get { return maximumItemLifespan; } set { if (value < TimeSpan.Zero) throw new ArgumentOutOfRangeException("value"); maximumItemLifespan = value; } } private TimeSpan cacheClearPeriod; public TimeSpan CacheClearPeriod { get { return cacheClearPeriod; } set { if (value < TimeSpan.Zero) throw new ArgumentOutOfRangeException("value"); cacheClearPeriod = value; } } public int ItemCount { get { lock (CacheLock) { return Contents.Count; } } } public TOut UseCache(TKey key, Func operation, Action onError, Predicate shouldRetryForException) { TOut output = default(TOut); try { output = operation(); } catch(Exception e) { // if predicate is specified, check whether to retry on exception // otherwise, retry var shouldRetry = shouldRetryForException == null || shouldRetryForException(e); if (shouldRetry) { // clear existing value Clear(key); // allow calling code to cleanup if (onError != null) onError(); // retry operation output = operation(); } else throw; } return output; } #endregion #region Private methods and classes private TValue GetValueHelper(TKey key, out bool isStaleItem, Func creator = null) { isStaleItem = true; CacheItem item = null; if (AWSConfigs.UseSdkCache) { lock (CacheLock) { if (!Contents.TryGetValue(key, out item) || !IsValidItem(item)) { if (creator == null) throw new InvalidOperationException("Unable to calculate value for key " + key); var value = creator(key); isStaleItem = false; item = new CacheItem(value); Contents[key] = item; RemoveOldItems_Locked(); } } } else { if (creator == null) throw new InvalidOperationException("Unable to calculate value for key " + key); var value = creator(key); item = new CacheItem(value); isStaleItem = false; } if (item == null) throw new InvalidOperationException("Unable to find value for key " + key); return item.Value; } private bool IsValidItem(CacheItem item) { if (item == null) return false; var cutoff = GetCorrectedLocalTime() - this.MaximumItemLifespan; if (item.LastUseTime < cutoff) return false; return true; } private void RemoveOldItems_Locked() { if (LastCacheClean + CacheClearPeriod > AWSConfigs.utcNowSource().ToLocalTime()) return; // Remove all items that were not accessed since the cutoff. // Using a cutoff is more optimal than item.Age, as we only need // to do DateTime calculation once, not for each item. var cutoff = GetCorrectedLocalTime() - MaximumItemLifespan; var keysToRemove = new List(); foreach (var kvp in Contents) { var key = kvp.Key; var item = kvp.Value; if (item == null || item.LastUseTime < cutoff) keysToRemove.Add(key); } foreach (var key in keysToRemove) Contents.Remove(key); LastCacheClean = GetCorrectedLocalTime(); } private class CacheItem { private T _value; public T Value { get { LastUseTime = GetCorrectedLocalTime(); return _value; } private set { _value = value; } } public DateTime LastUseTime { get; private set; } public CacheItem(T value) { Value = value; LastUseTime = GetCorrectedLocalTime(); } } private static DateTime GetCorrectedLocalTime() { #pragma warning disable CS0612 // Type or member is obsolete return AWSSDKUtils.CorrectedUtcNow.ToLocalTime(); #pragma warning restore CS0612 // Type or member is obsolete } #endregion } }