/* * Copyright 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.Globalization; using System.IO; using System.Linq; using System.Text; using Amazon.Runtime.Internal.Auth; using Amazon.Runtime.Internal.Util; namespace Amazon.Runtime.Internal { /// /// Class used to resolve endpoints using Endpoint Discovery. /// /// This class is only intended for internal use inside the AWS client libraries. /// Callers shouldn't ever interact directly with objects of this class. /// /// public abstract class EndpointDiscoveryResolverBase { private IClientConfig _config; private Logger _logger; private LruCache> _cache; private object objectCacheLock = new object(); private const int _cacheKeyValidityInSeconds = 3600; protected EndpointDiscoveryResolverBase(IClientConfig config, Logger logger) { _config = config; _logger = logger; _cache = new LruCache>(config.EndpointDiscoveryCacheLimit); } /// /// Method that performs endpoint discovery for the current operation /// /// Context information used in calculations for endpoint discovery /// The operation to fetch endpoints from the server /// public virtual IEnumerable ResolveEndpoints(EndpointOperationContextBase context, Func> InvokeEndpointOperation) { //Build the cacheKey var cacheKey = BuildEndpointDiscoveryCacheKey(context); //Evict cache keys that more than 1 hour old. _cache.EvictExpiredLRUListItems(_cacheKeyValidityInSeconds); //Check / cleanup the cache var refreshCache = false; IEnumerable endpoints = ProcessEndpointCache(cacheKey, context.EvictCacheKey, context.EvictUri, out refreshCache); if (endpoints != null) { if (refreshCache) { //Async fetch new endpoints because one or more of the endpoints in the cache have expired. #if AWS_ASYNC_API // Task only exists in framework 4.5 and up, and Standard. System.Threading.Tasks.Task.Run(() => { ProcessInvokeEndpointOperation(cacheKey, InvokeEndpointOperation, false); }); #else // ThreadPool only exists in 3.5 and below. These implementations do not have the Task library. System.Threading.ThreadPool.QueueUserWorkItem((state) => { ProcessInvokeEndpointOperation(cacheKey, InvokeEndpointOperation, false); }); #endif } return endpoints; } if (context.EvictCacheKey) { return null; } //Determine if we are required to get an endpoint or if we can defer it to an async call if (context.EndpointDiscoveryData.Required) { //Must find an endpoint or error for this operation endpoints = ProcessInvokeEndpointOperation(cacheKey, InvokeEndpointOperation, true); } else if (_config.EndpointDiscoveryEnabled) { //Optionally find and endpoint for this supported operation async #if AWS_ASYNC_API // Task only exists in framework 4.5 and up, and Standard. System.Threading.Tasks.Task.Run(() => { ProcessInvokeEndpointOperation(cacheKey, InvokeEndpointOperation, false); }); #else // ThreadPool only exists in 3.5 and below. These implementations do not have the Task library. System.Threading.ThreadPool.QueueUserWorkItem((state) => { ProcessInvokeEndpointOperation(cacheKey, InvokeEndpointOperation, false); }); #endif return null; } //else not required or endpoint discovery has been disabled so fall through to normal regional endpoint return endpoints; } private IEnumerable ProcessInvokeEndpointOperation(string cacheKey, Func> InvokeEndpointOperation, bool endpointRequired) { IList endpoints = null; try { endpoints = InvokeEndpointOperation(); if (endpoints != null && endpoints.Count > 0) { _cache.AddOrUpdate(cacheKey, endpoints); } else { endpoints = null; if (!endpointRequired) { //Since it is not required that endpoint discovery is used for this operation, cache //a null endpoint with an expiration time of 60 seconds so that requests are not //excessively sent. var cacheEndpoints = new List(); cacheEndpoints.Add(new DiscoveryEndpoint(null, 1)); _cache.AddOrUpdate(cacheKey, cacheEndpoints); } _logger.InfoFormat("The request to discover endpoints did not return any endpoints."); } } catch (Exception exception) { _logger.Error(exception, "An unhandled exception occurred while trying to discover endpoints."); } if (endpoints == null && endpointRequired) { throw new AmazonClientException("Failed to discover the endpoint for the request. Requests will not succeed until an endpoint can be retrieved or an endpoint is manually specified."); } return endpoints; } public virtual IList GetDiscoveryEndpointsFromCache(string cacheKey) { IList endpoints = null; if (!_cache.TryGetValue(cacheKey, out endpoints)) { return null; } return endpoints; } /// /// Gets the number of cache keys in the cache /// public virtual int CacheCount { get { return _cache.Count; } } private IEnumerable ProcessEndpointCache(string cacheKey, bool evictCacheKey, Uri evictUri, out bool refreshCache) { refreshCache = false; IList endpoints = GetDiscoveryEndpointsFromCache(cacheKey); if (evictCacheKey && endpoints != null) { //Force eviction of the evictUri endpoint but keep the rest to use. This only happens if an //Invalid Endpoint Exception was processed against the cached endpoint data. var testAddress = evictUri.ToString(); lock (objectCacheLock) { for (var i = 0; i < endpoints.Count; i++) { var endpoint = endpoints[i]; if (endpoint.Address != null && endpoint.Address.Equals(testAddress, StringComparison.OrdinalIgnoreCase)) { endpoints.RemoveAt(i); break; } } } if (endpoints.Count == 0) { _cache.Evict(cacheKey); return null; } } //Check to see if we have cached endpoints if (endpoints != null) { //If any endpoints have expired we need to async get new endpoints while continuing to use //the existing endpoints in the cache. They must not be evicted if they expired until new //endpoints are obtained. foreach (var endpoint in endpoints) { if (endpoint.HasExpired()) { refreshCache = true; //Add 1 minute to the expiration so we don't retry again until 1 minute from now if //the refresh fails. If the refresh succeeds all the extended endpoints will be replaced //before the extended 1 minute expiration. endpoint.ExtendExpiration(1); } } return endpoints; } return null; } private static string BuildEndpointDiscoveryCacheKey(EndpointOperationContextBase context) { var cacheKeyBuilder = new StringBuilder(); cacheKeyBuilder.Append(context.CustomerCredentials); var identifiers = context.EndpointDiscoveryData.Identifiers; if (identifiers != null && identifiers.Count > 0) { cacheKeyBuilder.Append(string.Format(CultureInfo.InvariantCulture, ".{0}", context.OperationName)); foreach (var identifier in identifiers) { cacheKeyBuilder.Append(string.Format(CultureInfo.InvariantCulture, ".{0}", identifier.Value)); } } return cacheKeyBuilder.ToString(); } } /// /// Class used to resolve endpoints using Endpoint Discovery. /// /// This class is only intended for internal use inside the AWS client libraries. /// Callers shouldn't ever interact directly with objects of this class. /// /// public class EndpointDiscoveryResolver : EndpointDiscoveryResolverBase { public EndpointDiscoveryResolver(IClientConfig config, Logger logger) : base(config, logger) { } } }