/*
* 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
}
}