This commit is contained in:
Tim Hess 2019-02-22 10:44:07 -06:00
Родитель ef66c7e8c3 40d06a5252
Коммит ffeeccded1
33 изменённых файлов: 1529 добавлений и 37 удалений

Просмотреть файл

@ -45,7 +45,7 @@ namespace Steeltoe.Common.Discovery
var current = request.RequestUri;
try
{
request.RequestUri = _discoveryBase.LookupService(current);
request.RequestUri = await _discoveryBase.LookupServiceAsync(current);
return await base.SendAsync(request, cancellationToken);
}
catch (Exception e)

Просмотреть файл

@ -13,19 +13,22 @@
// limitations under the License.
using Microsoft.Extensions.Logging;
using Steeltoe.Common.LoadBalancer;
using System;
using System.Threading.Tasks;
namespace Steeltoe.Common.Discovery
{
public class DiscoveryHttpClientHandlerBase
{
protected static Random _random = new Random();
protected IDiscoveryClient _client;
protected ILoadBalancer _loadBalancer;
protected ILogger _logger;
public DiscoveryHttpClientHandlerBase(IDiscoveryClient client, ILogger logger = null)
public DiscoveryHttpClientHandlerBase(IDiscoveryClient client, ILogger logger = null, ILoadBalancer loadBalancer = null)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_loadBalancer = loadBalancer ?? new RandomLoadBalancer(client);
_logger = logger;
}
@ -37,21 +40,18 @@ namespace Steeltoe.Common.Discovery
return current;
}
var instances = _client.GetInstances(current.Host);
if (instances.Count > 0)
return Task.Run(async () => await _loadBalancer.ResolveServiceInstanceAsync(current)).Result;
}
public virtual async Task<Uri> LookupServiceAsync(Uri current)
{
_logger?.LogDebug("LookupService({0})", current.ToString());
if (!current.IsDefaultPort)
{
var index = _random.Next(instances.Count);
var result = instances[index].Uri;
_logger?.LogDebug("Resolved {url} to {service}", current.Host, result.Host);
current = new Uri(result, current.PathAndQuery);
}
else
{
_logger?.LogWarning("Attempted to resolve service for {url} but found 0 instances", current.Host);
return current;
}
_logger?.LogDebug("LookupService() returning {0} ", current.ToString());
return current;
return await _loadBalancer.ResolveServiceInstanceAsync(current);
}
}
}

Просмотреть файл

@ -46,7 +46,7 @@ namespace Steeltoe.Common.Http.Discovery
var current = request.RequestUri;
try
{
request.RequestUri = _discoveryBase.LookupService(current);
request.RequestUri = await _discoveryBase.LookupServiceAsync(current);
return await base.SendAsync(request, cancellationToken);
}
catch (Exception e)

Просмотреть файл

@ -0,0 +1,94 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Steeltoe.Common.LoadBalancer;
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Steeltoe.Common.Http.LoadBalancer
{
/// <summary>
/// Same as <see cref="LoadBalancerHttpClientHandler"/> except is a <see cref="DelegatingHandler"/>, for use with HttpClientFactory
/// </summary>
public class LoadBalancerDelegatingHandler : DelegatingHandler
{
private readonly ILoadBalancer _loadBalancer;
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="LoadBalancerDelegatingHandler"/> class. <para />
/// For use with <see cref="IHttpClientBuilder"/>
/// </summary>
/// <param name="loadBalancer">Load balancer to use</param>
/// <param name="logger">For logging</param>
public LoadBalancerDelegatingHandler(ILoadBalancer loadBalancer, ILogger logger = null)
{
_loadBalancer = loadBalancer ?? throw new ArgumentNullException(nameof(loadBalancer));
_logger = logger;
}
/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// record the original request
var originalUri = request.RequestUri;
Uri resolvedUri = null;
DateTime startTime = default(DateTime);
DateTime endTime = default(DateTime);
try
{
// look up a service instance and update the request
resolvedUri = await _loadBalancer.ResolveServiceInstanceAsync(request.RequestUri);
request.RequestUri = resolvedUri;
// allow other handlers to operate and the request to continue
startTime = DateTime.UtcNow;
var response = await base.SendAsync(request, cancellationToken);
endTime = DateTime.UtcNow;
// track stats
await _loadBalancer.UpdateStatsAsync(originalUri, resolvedUri, endTime - startTime, null);
return response;
}
catch (Exception exception)
{
if (endTime == default(DateTime))
{
endTime = DateTime.UtcNow;
}
_logger?.LogDebug(exception, "Exception during SendAsync()");
if (resolvedUri != null)
{
await _loadBalancer.UpdateStatsAsync(originalUri, resolvedUri, endTime - startTime, exception);
}
else
{
_logger?.LogWarning("resolvedUri was null. This might be an issue with your service discovery provider.");
}
throw;
}
finally
{
request.RequestUri = originalUri;
}
}
}
}

Просмотреть файл

@ -0,0 +1,85 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Steeltoe.Common.Discovery;
using Steeltoe.Common.Http.LoadBalancer;
using Steeltoe.Common.LoadBalancer;
using System;
using System.Net.Http;
namespace Microsoft.Extensions.DependencyInjection
{
public static class LoadBalancerHttpClientBuilderExtensions
{
/// <summary>
/// Adds a <see cref="DelegatingHandler"/> that performs random load balancing
/// </summary>
/// <param name="httpClientBuilder">The <see cref="IHttpClientBuilder"/>.</param>
/// <remarks>Requires an <see cref="IServiceInstanceProvider" /> or <see cref="IDiscoveryClient"/> in the DI container so the load balancer can sent traffic to more than one address</remarks>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
public static IHttpClientBuilder AddRandomLoadBalancer(this IHttpClientBuilder httpClientBuilder)
{
if (httpClientBuilder == null)
{
throw new ArgumentNullException(nameof(httpClientBuilder));
}
httpClientBuilder.Services.TryAddSingleton(typeof(RandomLoadBalancer));
return httpClientBuilder.AddLoadBalancer<RandomLoadBalancer>();
}
/// <summary>
/// Adds a <see cref="DelegatingHandler"/> that performs round robin load balancing, optionally backed by an <see cref="IDistributedCache"/>
/// </summary>
/// <param name="httpClientBuilder">The <see cref="IHttpClientBuilder"/>.</param>
/// <remarks>
/// Requires an <see cref="IServiceInstanceProvider" /> or <see cref="IDiscoveryClient"/> in the DI container so the load balancer can sent traffic to more than one address<para />
/// Also requires an <see cref="IDistributedCache"/> in the DI Container for consistent round robin balancing across multiple client instances
/// </remarks>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
public static IHttpClientBuilder AddRoundRobinLoadBalancer(this IHttpClientBuilder httpClientBuilder)
{
if (httpClientBuilder == null)
{
throw new ArgumentNullException(nameof(httpClientBuilder));
}
httpClientBuilder.Services.TryAddSingleton(typeof(RoundRobinLoadBalancer));
return httpClientBuilder.AddLoadBalancer<RoundRobinLoadBalancer>();
}
/// <summary>
/// Adds an <see cref="HttpMessageHandler"/> with specified load balancer <para/>
/// Does NOT add the specified load balancer to the container. Please add your load balancer separately.
/// </summary>
/// <param name="httpClientBuilder">The <see cref="IHttpClientBuilder"/>.</param>
/// <typeparam name="T">The type of <see cref="ILoadBalancer"/> to use</typeparam>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
public static IHttpClientBuilder AddLoadBalancer<T>(this IHttpClientBuilder httpClientBuilder)
where T : ILoadBalancer
{
if (httpClientBuilder == null)
{
throw new ArgumentNullException(nameof(httpClientBuilder));
}
httpClientBuilder.Services.TryAddTransient<LoadBalancerDelegatingHandler>();
httpClientBuilder.AddHttpMessageHandler((services) => new LoadBalancerDelegatingHandler(services.GetRequiredService<T>(), services.GetService<ILogger>()));
return httpClientBuilder;
}
}
}

Просмотреть файл

@ -0,0 +1,93 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 Microsoft.Extensions.Logging;
using Steeltoe.Common.LoadBalancer;
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Steeltoe.Common.Http.LoadBalancer
{
/// <summary>
/// Same as <see cref="LoadBalancerDelegatingHandler"/> except is an <see cref="HttpClientHandler"/>, for non-HttpClientFactory use
/// </summary>
public class LoadBalancerHttpClientHandler : HttpClientHandler
{
private readonly ILoadBalancer _loadBalancer;
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="LoadBalancerHttpClientHandler"/> class. <para />
/// For use with <see cref="HttpClient"/> without <see cref="IHttpClientFactory"/>
/// </summary>
/// <param name="loadBalancer">Load balancer to use</param>
/// <param name="logger">For logging</param>
public LoadBalancerHttpClientHandler(ILoadBalancer loadBalancer, ILogger logger = null)
{
_loadBalancer = loadBalancer ?? throw new ArgumentNullException(nameof(loadBalancer));
_logger = logger;
}
/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// record the original request
var originalUri = request.RequestUri;
Uri resolvedUri = null;
DateTime startTime = default(DateTime);
DateTime endTime = default(DateTime);
try
{
// look up a service instance and update the request
resolvedUri = await _loadBalancer.ResolveServiceInstanceAsync(request.RequestUri);
request.RequestUri = resolvedUri;
// allow other handlers to operate and the request to continue
startTime = DateTime.UtcNow;
var response = await base.SendAsync(request, cancellationToken);
endTime = DateTime.UtcNow;
// track stats
await _loadBalancer.UpdateStatsAsync(originalUri, resolvedUri, endTime - startTime, null);
return response;
}
catch (Exception exception)
{
if (endTime == default(DateTime))
{
endTime = DateTime.UtcNow;
}
_logger?.LogDebug(exception, "Exception during SendAsync()");
if (resolvedUri != null)
{
await _loadBalancer.UpdateStatsAsync(originalUri, resolvedUri, endTime - startTime, exception);
}
else
{
_logger?.LogWarning("resolvedUri was null. This might be an issue with your service discovery provider.");
}
throw;
}
finally
{
request.RequestUri = originalUri;
}
}
}
}

Просмотреть файл

@ -0,0 +1,34 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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;
namespace Steeltoe.Common.Discovery
{
public class ConfigurationServiceInstance : IServiceInstance
{
public string ServiceId { get; set; }
public string Host { get; set; }
public int Port { get; set; }
public bool IsSecure { get; set; }
public Uri Uri => new Uri((IsSecure ? Uri.UriSchemeHttps : Uri.UriSchemeHttp) + Uri.SchemeDelimiter + Host + ':' + Port);
public IDictionary<string, string> Metadata { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,40 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Steeltoe.Common.Discovery
{
public class ConfigurationServiceInstanceProvider : IServiceInstanceProvider
{
private readonly IOptionsMonitor<List<ConfigurationServiceInstance>> _serviceInstances;
public ConfigurationServiceInstanceProvider(IOptionsMonitor<List<ConfigurationServiceInstance>> serviceInstances)
{
_serviceInstances = serviceInstances;
}
public string Description => "A service instance provider that returns services from app configuration";
public IList<string> Services => _serviceInstances.CurrentValue.Select(si => si.ServiceId).Distinct().ToList();
public IList<IServiceInstance> GetInstances(string serviceId)
{
return new List<IServiceInstance>(_serviceInstances.CurrentValue.Where(si => si.ServiceId.Equals(serviceId, StringComparison.InvariantCultureIgnoreCase)));
}
}
}

Просмотреть файл

@ -0,0 +1,49 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 Microsoft.Extensions.Configuration;
using Steeltoe.Common.Discovery;
using System;
using System.Collections.Generic;
namespace Microsoft.Extensions.DependencyInjection
{
public static class ConfigurationServiceInstanceProviderServiceCollectionExtensions
{
/// <summary>
/// Adds an IConfiguration-based <see cref="IServiceInstanceProvider"/> to the <see cref="IServiceCollection" />
/// </summary>
/// <param name="services">Your <see cref="IServiceCollection"/></param>
/// <param name="configuration">Application configuration</param>
/// <param name="serviceLifetime">Lifetime of the <see cref="IServiceInstanceProvider"/></param>
/// <returns>IServiceCollection for chaining</returns>
public static IServiceCollection AddConfigurationDiscoveryClient(this IServiceCollection services, IConfiguration configuration, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
services.Add(new ServiceDescriptor(typeof(IServiceInstanceProvider), typeof(ConfigurationServiceInstanceProvider), serviceLifetime));
services.AddOptions();
services.Configure<List<ConfigurationServiceInstance>>(configuration.GetSection("discovery:services"));
return services;
}
}
}

Просмотреть файл

@ -12,36 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Steeltoe.Common.Discovery
{
public interface IDiscoveryClient
public interface IDiscoveryClient : IServiceInstanceProvider
{
/// <summary>
/// Gets a human readable description of the implementation
/// </summary>
string Description { get; }
/// <summary>
/// Gets all known service Ids
/// </summary>
IList<string> Services { get; }
/// <summary>
/// ServiceInstance with information used to register the local service
/// </summary>
/// <returns>The IServiceInstance</returns>
IServiceInstance GetLocalServiceInstance();
/// <summary>
/// Get all ServiceInstances associated with a particular serviceId
/// </summary>
/// <param name="serviceId">the serviceId to lookup</param>
/// <returns>List of service instances</returns>
IList<IServiceInstance> GetInstances(string serviceId);
Task ShutdownAsync();
}
}

Просмотреть файл

@ -0,0 +1,38 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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.Collections.Generic;
namespace Steeltoe.Common.Discovery
{
public interface IServiceInstanceProvider
{
/// <summary>
/// Gets a human readable description of the implementation
/// </summary>
string Description { get; }
/// <summary>
/// Gets all known service Ids
/// </summary>
IList<string> Services { get; }
/// <summary>
/// Get all ServiceInstances associated with a particular serviceId
/// </summary>
/// <param name="serviceId">the serviceId to lookup</param>
/// <returns>List of service instances</returns>
IList<IServiceInstance> GetInstances(string serviceId);
}
}

Просмотреть файл

@ -0,0 +1,73 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 Microsoft.Extensions.Caching.Distributed;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading.Tasks;
namespace Steeltoe.Common.Discovery
{
public static class IServiceInstanceProviderExtensions
{
public static async Task<IList<IServiceInstance>> GetInstancesWithCacheAsync(this IServiceInstanceProvider serviceInstanceProvider, string serviceId, IDistributedCache distributedCache = null, string serviceInstancesKeyPrefix = "ServiceInstances-")
{
// if distributed cache was provided, just make the call back to the provider
if (distributedCache != null)
{
// check the cache for existing service instances
var instanceData = await distributedCache.GetAsync(serviceInstancesKeyPrefix + serviceId);
if (instanceData != null && instanceData.Length > 0)
{
return DeserializeFromCache<List<SerializableIServiceInstance>>(instanceData).ToList<IServiceInstance>();
}
}
// cache not found or instances not found, call out to the provider
var instances = serviceInstanceProvider.GetInstances(serviceId);
if (distributedCache != null)
{
await distributedCache.SetAsync(serviceInstancesKeyPrefix + serviceId, SerializeForCache(MapToSerializable(instances)));
}
return instances;
}
private static List<SerializableIServiceInstance> MapToSerializable(IList<IServiceInstance> instances)
{
var inst = instances.Select(i => new SerializableIServiceInstance(i));
return inst.ToList();
}
private static byte[] SerializeForCache(object data)
{
using (var stream = new MemoryStream())
{
new BinaryFormatter().Serialize(stream, data);
return stream.ToArray();
}
}
private static T DeserializeFromCache<T>(byte[] data)
where T : class
{
using (var stream = new MemoryStream(data))
{
return new BinaryFormatter().Deserialize(stream) as T;
}
}
}
}

Просмотреть файл

@ -0,0 +1,45 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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;
namespace Steeltoe.Common.Discovery
{
[Serializable]
public class SerializableIServiceInstance : IServiceInstance
{
public SerializableIServiceInstance(IServiceInstance instance)
{
ServiceId = instance.ServiceId;
Host = instance.Host;
Port = instance.Port;
IsSecure = instance.IsSecure;
Uri = instance.Uri;
Metadata = instance.Metadata;
}
public string ServiceId { get; set; }
public string Host { get; set; }
public int Port { get; set; }
public bool IsSecure { get; set; }
public Uri Uri { get; set; }
public IDictionary<string, string> Metadata { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,26 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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.Threading.Tasks;
namespace Steeltoe.Common.LoadBalancer
{
public interface ILoadBalancer
{
Task<Uri> ResolveServiceInstanceAsync(Uri request);
Task UpdateStatsAsync(Uri originalUri, Uri resolvedUri, TimeSpan responseTime, Exception exception);
}
}

Просмотреть файл

@ -0,0 +1,66 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Steeltoe.Common.Discovery;
using System;
using System.Threading.Tasks;
namespace Steeltoe.Common.LoadBalancer
{
public class RandomLoadBalancer : ILoadBalancer
{
private static readonly Random _random = new Random();
private readonly IServiceInstanceProvider _serviceInstanceProvider;
private readonly IDistributedCache _distributedCache;
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="RandomLoadBalancer"/> class.
/// Returns random service instances, with option caching of service lookups
/// </summary>
/// <param name="serviceInstanceProvider">Provider of service instance information</param>
/// <param name="distributedCache">For caching service instance data</param>
/// <param name="logger">For logging</param>
public RandomLoadBalancer(IServiceInstanceProvider serviceInstanceProvider, IDistributedCache distributedCache = null, ILogger logger = null)
{
_serviceInstanceProvider = serviceInstanceProvider ?? throw new ArgumentNullException(nameof(serviceInstanceProvider));
_distributedCache = distributedCache;
_logger = logger;
}
public virtual async Task<Uri> ResolveServiceInstanceAsync(Uri request)
{
_logger?.LogTrace("ResolveServiceInstance {serviceInstance}", request.Host);
var availableServiceInstances = await _serviceInstanceProvider.GetInstancesWithCacheAsync(request.Host, _distributedCache);
if (availableServiceInstances.Count > 0)
{
var resolvedUri = availableServiceInstances[_random.Next(availableServiceInstances.Count)].Uri;
_logger?.LogDebug("Resolved {url} to {service}", request.Host, resolvedUri.Host);
return new Uri(resolvedUri, request.PathAndQuery);
}
else
{
_logger?.LogWarning("Attempted to resolve service for {url} but found 0 instances", request.Host);
return request;
}
}
public virtual Task UpdateStatsAsync(Uri originalUri, Uri resolvedUri, TimeSpan responseTime, Exception exception)
{
return Task.CompletedTask;
}
}
}

Просмотреть файл

@ -0,0 +1,104 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Steeltoe.Common.Discovery;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
namespace Steeltoe.Common.LoadBalancer
{
public class RoundRobinLoadBalancer : ILoadBalancer
{
public string IndexKeyPrefix = "LoadBalancerIndex-";
internal readonly IServiceInstanceProvider ServiceInstanceProvider;
internal readonly IDistributedCache _distributedCache;
internal readonly ConcurrentDictionary<string, int> NextIndexForService = new ConcurrentDictionary<string, int>();
private readonly ILogger _logger;
public RoundRobinLoadBalancer(IServiceInstanceProvider serviceInstanceProvider, IDistributedCache distributedCache = null, ILogger logger = null)
{
ServiceInstanceProvider = serviceInstanceProvider ?? throw new ArgumentNullException(nameof(serviceInstanceProvider));
_distributedCache = distributedCache;
_logger = logger;
_logger?.LogDebug("Distributed cache was provided to load balancer: {DistributedCacheIsNull}", _distributedCache == null);
}
public virtual async Task<Uri> ResolveServiceInstanceAsync(Uri request)
{
var serviceName = request.Host;
_logger?.LogTrace("ResolveServiceInstance {serviceName}", serviceName);
string cacheKey = IndexKeyPrefix + serviceName;
// get instances for this service
var availableServiceInstances = await ServiceInstanceProvider.GetInstancesWithCacheAsync(serviceName, _distributedCache);
if (!availableServiceInstances.Any())
{
_logger?.LogError("No service instances available for {serviceName}", serviceName);
return request;
}
// get next instance, or wrap back to first instance if we reach the end of the list
IServiceInstance serviceInstance = null;
var nextInstanceIndex = await GetOrInitNextIndex(cacheKey, 0);
if (nextInstanceIndex >= availableServiceInstances.Count)
{
nextInstanceIndex = 0;
}
serviceInstance = availableServiceInstances[nextInstanceIndex];
await SetNextIndex(cacheKey, nextInstanceIndex);
return new Uri(serviceInstance.Uri, request.PathAndQuery);
}
public virtual Task UpdateStatsAsync(Uri originalUri, Uri resolvedUri, TimeSpan responseTime, Exception exception)
{
return Task.CompletedTask;
}
private async Task<int> GetOrInitNextIndex(string cacheKey, int initValue)
{
int index = initValue;
if (_distributedCache != null)
{
var cacheEntry = await _distributedCache.GetAsync(cacheKey);
if (cacheEntry != null && cacheEntry.Length > 0)
{
index = BitConverter.ToInt16(cacheEntry, 0);
}
}
else
{
index = NextIndexForService.GetOrAdd(cacheKey, initValue);
}
return index;
}
private async Task SetNextIndex(string cacheKey, int currentValue)
{
if (_distributedCache != null)
{
await _distributedCache.SetAsync(cacheKey, BitConverter.GetBytes(currentValue + 1));
}
else
{
NextIndexForService[cacheKey] = currentValue + 1;
}
}
}
}

Просмотреть файл

@ -0,0 +1,17 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Steeltoe.Common.Test")]

Просмотреть файл

@ -22,8 +22,10 @@
<PropertyGroup>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="$(ExtensionsVersion)" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="$(ExtensionsVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(ExtensionsVersion)" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="$(ExtensionsVersion)" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="$(DiagnosticSourceVersion)" />
<PackageReference Include="StyleCop.Analyzers" Version="$(StyleCopVersion)">
<PrivateAssets>All</PrivateAssets>

Просмотреть файл

@ -14,8 +14,6 @@
using Steeltoe.Common.Discovery;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;
namespace Steeltoe.Common.Http.Test
@ -71,5 +69,44 @@ namespace Steeltoe.Common.Http.Test
var result = handler.LookupService(uri);
Assert.Equal(new Uri("http://foundit:5555/test/bar/foo?test=1&test2=2"), result);
}
[Fact]
public async void LookupServiceAsync_NonDefaultPort_ReturnsOriginalURI()
{
// Arrange
IDiscoveryClient client = new TestDiscoveryClient();
DiscoveryHttpClientHandlerBase handler = new DiscoveryHttpClientHandlerBase(client);
Uri uri = new Uri("http://foo:8080/test");
// Act and Assert
var result = await handler.LookupServiceAsync(uri);
Assert.Equal(uri, result);
}
[Fact]
public async void LookupServiceAsync_DoesntFindService_ReturnsOriginalURI()
{
// Arrange
IDiscoveryClient client = new TestDiscoveryClient();
DiscoveryHttpClientHandlerBase handler = new DiscoveryHttpClientHandlerBase(client);
Uri uri = new Uri("http://foo/test");
// Act and Assert
var result = await handler.LookupServiceAsync(uri);
Assert.Equal(uri, result);
}
[Fact]
public async void LookupServiceAsync_FindsService_ReturnsURI()
{
// Arrange
IDiscoveryClient client = new TestDiscoveryClient(new TestServiceInstance(new Uri("http://foundit:5555")));
DiscoveryHttpClientHandlerBase handler = new DiscoveryHttpClientHandlerBase(client);
Uri uri = new Uri("http://foo/test/bar/foo?test=1&test2=2");
// Act and Assert
var result = await handler.LookupServiceAsync(uri);
Assert.Equal(new Uri("http://foundit:5555/test/bar/foo?test=1&test2=2"), result);
}
}
}

Просмотреть файл

@ -0,0 +1,45 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 Steeltoe.Common.LoadBalancer;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Steeltoe.Common.Http.LoadBalancer.Test
{
internal class BrokenLoadBalancer : ILoadBalancer
{
internal List<Tuple<Uri, Uri, TimeSpan, Exception>> Stats = new List<Tuple<Uri, Uri, TimeSpan, Exception>>();
/// <summary>
/// Initializes a new instance of the <see cref="BrokenLoadBalancer"/> class.
/// Throws exceptions when you try to resolve services
/// </summary>
public BrokenLoadBalancer()
{
}
public Task<Uri> ResolveServiceInstanceAsync(Uri request)
{
throw new Exception("(╯°□°)╯︵ ┻━┻");
}
public Task UpdateStatsAsync(Uri originalUri, Uri resolvedUri, TimeSpan responseTime, Exception exception)
{
Stats.Add(new Tuple<Uri, Uri, TimeSpan, Exception>(originalUri, resolvedUri, responseTime, exception));
return Task.CompletedTask;
}
}
}

Просмотреть файл

@ -0,0 +1,48 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 Steeltoe.Common.LoadBalancer;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Steeltoe.Common.Http.LoadBalancer.Test
{
/// <summary>
/// A bad fake load balancer that only resolves requests for "replaceme" as "someresolvedhost"
/// </summary>
internal class FakeLoadBalancer : ILoadBalancer
{
internal List<Tuple<Uri, Uri, TimeSpan, Exception>> Stats = new List<Tuple<Uri, Uri, TimeSpan, Exception>>();
/// <summary>
/// Initializes a new instance of the <see cref="FakeLoadBalancer"/> class.
/// Only capable of resolving requests for "replaceme" as "someresolvedhost"
/// </summary>
public FakeLoadBalancer()
{
}
public Task<Uri> ResolveServiceInstanceAsync(Uri request)
{
return Task.FromResult(new Uri(request.AbsoluteUri.Replace("replaceme", "someresolvedhost")));
}
public Task UpdateStatsAsync(Uri originalUri, Uri resolvedUri, TimeSpan responseTime, Exception exception)
{
Stats.Add(new Tuple<Uri, Uri, TimeSpan, Exception>(originalUri, resolvedUri, responseTime, exception));
return Task.CompletedTask;
}
}
}

Просмотреть файл

@ -0,0 +1,85 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 Steeltoe.Common.Http.Test;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using Xunit;
namespace Steeltoe.Common.Http.LoadBalancer.Test
{
public class LoadBalancerDelegatingHandlerTest
{
[Fact]
public void Throws_If_LoadBalancerNull()
{
var exception = Assert.Throws<ArgumentNullException>(() => new LoadBalancerDelegatingHandler(null));
Assert.Equal("loadBalancer", exception.ParamName);
}
[Fact]
public async void ResolvesUri_TracksStats_WithProvidedLoadBalancer()
{
// arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://replaceme/api");
var loadBalancer = new FakeLoadBalancer();
var handler = new LoadBalancerDelegatingHandler(loadBalancer) { InnerHandler = new TestInnerDelegatingHandler() };
var invoker = new HttpMessageInvoker(handler);
// act
var result = await invoker.SendAsync(httpRequestMessage, default(CancellationToken));
// assert
Assert.Equal("http://someresolvedhost/api", result.Headers.GetValues("requestUri").First());
Assert.Single(loadBalancer.Stats);
}
[Fact]
public async void DoesntTrackStats_WhenResolutionFails_WithProvidedLoadBalancer()
{
// arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://replaceme/api");
var loadBalancer = new BrokenLoadBalancer();
var handler = new LoadBalancerDelegatingHandler(loadBalancer) { InnerHandler = new TestInnerDelegatingHandler() };
var invoker = new HttpMessageInvoker(handler);
// act
var result = await Assert.ThrowsAsync<Exception>(async () => await invoker.SendAsync(httpRequestMessage, default(CancellationToken)));
// assert
Assert.Empty(loadBalancer.Stats);
}
[Fact]
public async void TracksStats_WhenRequestsGoWrong_WithProvidedLoadBalancer()
{
// arrange
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "http://replaceme/api");
var loadBalancer = new FakeLoadBalancer();
var handler = new LoadBalancerDelegatingHandler(loadBalancer) { InnerHandler = new TestInnerDelegatingHandlerBrokenServer() };
var invoker = new HttpMessageInvoker(handler);
// act
var result = await invoker.SendAsync(httpRequestMessage, default(CancellationToken));
// assert
Assert.Single(loadBalancer.Stats);
Assert.Equal(HttpStatusCode.InternalServerError, result.StatusCode);
Assert.Equal("http://someresolvedhost/api", result.Headers.GetValues("requestUri").First());
}
}
}

Просмотреть файл

@ -0,0 +1,148 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Steeltoe.Common.LoadBalancer;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using Xunit;
namespace Steeltoe.Common.Http.LoadBalancer.Test
{
public class LoadBalancerHttpClientBuilderExtensionsTest
{
[Fact]
public void AddRandomLoadBalancer_ThrowsIfBuilderNull()
{
var exception = Assert.Throws<ArgumentNullException>(() => LoadBalancerHttpClientBuilderExtensions.AddRandomLoadBalancer(null));
Assert.Equal("httpClientBuilder", exception.ParamName);
}
[Fact]
public void AddRandomLoadBalancer_AddsRandomLoadBalancerToServices()
{
// arrange
var services = new ServiceCollection();
services.AddConfigurationDiscoveryClient(new ConfigurationBuilder().Build());
// act
services.AddHttpClient("test").AddRandomLoadBalancer();
var serviceProvider = services.BuildServiceProvider();
var serviceEntryInCollection = services.FirstOrDefault(service => service.ServiceType.Equals(typeof(RandomLoadBalancer)));
// assert
Assert.Single(serviceProvider.GetServices<RandomLoadBalancer>());
Assert.NotNull(serviceEntryInCollection);
Assert.Equal(ServiceLifetime.Singleton, serviceEntryInCollection.Lifetime);
}
[Fact]
public void AddRoundRobinLoadBalancer_ThrowsIfBuilderNull()
{
var exception = Assert.Throws<ArgumentNullException>(() => LoadBalancerHttpClientBuilderExtensions.AddRoundRobinLoadBalancer(null));
Assert.Equal("httpClientBuilder", exception.ParamName);
}
[Fact]
public void AddRoundRobinLoadBalancer_AddsRoundRobinLoadBalancerToServices()
{
// arrange
var services = new ServiceCollection();
services.AddConfigurationDiscoveryClient(new ConfigurationBuilder().Build());
// act
services.AddHttpClient("test").AddRoundRobinLoadBalancer();
var serviceProvider = services.BuildServiceProvider();
var serviceEntryInCollection = services.FirstOrDefault(service => service.ServiceType.Equals(typeof(RoundRobinLoadBalancer)));
// assert
Assert.Single(serviceProvider.GetServices<RoundRobinLoadBalancer>());
Assert.Equal(ServiceLifetime.Singleton, serviceEntryInCollection.Lifetime);
}
[Fact]
public void AddLoadBalancerT_ThrowsIfBuilderNull()
{
var exception = Assert.Throws<ArgumentNullException>(() => LoadBalancerHttpClientBuilderExtensions.AddLoadBalancer<FakeLoadBalancer>(null));
Assert.Equal("httpClientBuilder", exception.ParamName);
}
[Fact]
public void AddLoadBalancerT_DoesntAddT_ToServices()
{
// arrange
var services = new ServiceCollection();
// act
services.AddHttpClient("test").AddLoadBalancer<FakeLoadBalancer>();
var serviceProvider = services.BuildServiceProvider();
// assert
Assert.Empty(serviceProvider.GetServices<FakeLoadBalancer>());
}
[Fact]
public void AddLoadBalancerT_CanBeUsedWithAnHttpClient()
{
// arrange
var services = new ServiceCollection();
services.AddSingleton(typeof(FakeLoadBalancer));
// act
services.AddHttpClient("test").AddLoadBalancer<FakeLoadBalancer>();
var serviceProvider = services.BuildServiceProvider();
var factory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient("test");
// assert
Assert.NotNull(client);
}
[Fact]
public void CanAddMultipleLoadBalancers()
{
// arrange
var services = new ServiceCollection();
services.AddConfigurationDiscoveryClient(new ConfigurationBuilder().Build());
services.AddSingleton(typeof(FakeLoadBalancer));
// act
services.AddHttpClient("testRandom").AddRandomLoadBalancer();
services.AddHttpClient("testRandom2").AddRandomLoadBalancer();
services.AddHttpClient("testRoundRobin").AddRoundRobinLoadBalancer();
services.AddHttpClient("testRoundRobin2").AddRoundRobinLoadBalancer();
services.AddHttpClient("testFake").AddLoadBalancer<FakeLoadBalancer>();
services.AddHttpClient("testFake2").AddLoadBalancer<FakeLoadBalancer>();
var factory = services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();
var randomLBClient = factory.CreateClient("testRandom");
var randomLBClient2 = factory.CreateClient("testRandom2");
var roundRobinLBClient = factory.CreateClient("testRoundRobin");
var roundRobinLBClient2 = factory.CreateClient("testRoundRobin2");
var fakeLBClient = factory.CreateClient("testFake");
var fakeLBClient2 = factory.CreateClient("testFake2");
// assert
Assert.NotNull(randomLBClient);
Assert.NotNull(randomLBClient2);
Assert.NotNull(roundRobinLBClient);
Assert.NotNull(roundRobinLBClient2);
Assert.NotNull(fakeLBClient);
Assert.NotNull(fakeLBClient2);
}
}
}

Просмотреть файл

@ -24,6 +24,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(ExtensionsVersion)" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(ExtensionsVersion)" />
<PackageReference Include="RichardSzalay.MockHttp" Version="$(MockHttpVersion)" />
<PackageReference Include="xunit" Version="$(XunitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitStudioVersion)" />

Просмотреть файл

@ -0,0 +1,31 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Steeltoe.Common.Http.Test
{
public class TestInnerDelegatingHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var responseMessage = new HttpResponseMessage(HttpStatusCode.OK);
responseMessage.Headers.Add("requestUri", request.RequestUri.AbsoluteUri);
return Task.Factory.StartNew(() => responseMessage, cancellationToken);
}
}
}

Просмотреть файл

@ -0,0 +1,31 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Steeltoe.Common.Http.Test
{
public class TestInnerDelegatingHandlerBrokenServer : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var responseMessage = new HttpResponseMessage(HttpStatusCode.InternalServerError);
responseMessage.Headers.Add("requestUri", request.RequestUri.AbsoluteUri);
return Task.Factory.StartNew(() => responseMessage, cancellationToken);
}
}
}

Просмотреть файл

@ -0,0 +1,62 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.IO;
using Xunit;
namespace Steeltoe.Common.Discovery.Test
{
public class ConfigurationServiceInstanceProviderServiceCollectionExtensionsTest
{
[Fact]
public void AddConfigurationDiscoveryClient_AddsClientWithOptions()
{
// arrange
var appsettings = @"
{
'discovery': {
'services': [
{ 'serviceId': 'fruitService', 'host': 'fruitball', 'port': 443, 'isSecure': true },
{ 'serviceId': 'fruitService', 'host': 'fruitballer', 'port': 8081 },
{ 'serviceId': 'vegetableService', 'host': 'vegemite', 'port': 443, 'isSecure': true },
{ 'serviceId': 'vegetableService', 'host': 'carrot', 'port': 8081 },
]
}
}";
var path = TestHelpers.CreateTempFile(appsettings);
string directory = Path.GetDirectoryName(path);
string fileName = Path.GetFileName(path);
var cbuilder = new ConfigurationBuilder();
cbuilder.SetBasePath(directory);
cbuilder.AddJsonFile(fileName);
var services = new ServiceCollection();
// act
services.AddConfigurationDiscoveryClient(cbuilder.Build());
var serviceProvider = services.BuildServiceProvider();
// by getting the provider, we're confirming that the options are also available in the container
var serviceInstanceProvider = serviceProvider.GetRequiredService(typeof(IServiceInstanceProvider)) as IServiceInstanceProvider;
// assert
Assert.NotNull(serviceInstanceProvider);
Assert.IsType<ConfigurationServiceInstanceProvider>(serviceInstanceProvider);
Assert.Equal(2, serviceInstanceProvider.Services.Count);
Assert.Equal(2, serviceInstanceProvider.GetInstances("fruitService").Count);
Assert.Equal(2, serviceInstanceProvider.GetInstances("vegetableService").Count);
}
}
}

Просмотреть файл

@ -0,0 +1,67 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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.Collections.Generic;
using System.Linq;
using Xunit;
namespace Steeltoe.Common.Discovery.Test
{
public class ConfigurationServiceInstanceProviderTest
{
[Fact]
public void Returns_ConfiguredServices()
{
// arrange
var services = new List<ConfigurationServiceInstance>
{
new ConfigurationServiceInstance { ServiceId = "fruitService", Host = "fruitball", Port = 443, IsSecure = true },
new ConfigurationServiceInstance { ServiceId = "fruitService", Host = "fruitballer", Port = 8081 },
new ConfigurationServiceInstance { ServiceId = "fruitService", Host = "fruitballerz", Port = 8082 },
new ConfigurationServiceInstance { ServiceId = "vegetableService", Host = "vegemite", Port = 443, IsSecure = true },
new ConfigurationServiceInstance { ServiceId = "vegetableService", Host = "carrot", Port = 8081 },
new ConfigurationServiceInstance { ServiceId = "vegetableService", Host = "beet", Port = 8082 },
};
var serviceOptions = new TestOptionsMonitor<List<ConfigurationServiceInstance>>(services);
// act
var provider = new ConfigurationServiceInstanceProvider(serviceOptions);
// assert
Assert.Equal(3, provider.GetInstances("fruitService").Count);
Assert.Equal(3, provider.GetInstances("vegetableService").Count);
Assert.Equal(2, provider.Services.Count);
}
[Fact]
public void ReceivesUpdatesTo_ConfiguredServices()
{
// arrange
var services = new List<ConfigurationServiceInstance>
{
new ConfigurationServiceInstance { ServiceId = "fruitService", Host = "fruitball", Port = 443, IsSecure = true },
};
var serviceOptions = new TestOptionsMonitor<List<ConfigurationServiceInstance>>(services);
var provider = new ConfigurationServiceInstanceProvider(serviceOptions);
Assert.Single(provider.GetInstances("fruitService"));
Assert.Equal("fruitball", provider.GetInstances("fruitService").First().Host);
// act
services.First().Host = "updatedValue";
// assert
Assert.Equal("updatedValue", provider.GetInstances("fruitService").First().Host);
}
}
}

Просмотреть файл

@ -0,0 +1,100 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Steeltoe.Common.Discovery;
using System;
using System.Collections.Generic;
using Xunit;
namespace Steeltoe.Common.LoadBalancer.Test
{
public class RoundRobinLoadBalancerTest
{
[Fact]
public void Throws_If_IServiceInstanceProviderNotProvided()
{
var exception = Assert.Throws<ArgumentNullException>(() => new RoundRobinLoadBalancer(null));
Assert.Equal("serviceInstanceProvider", exception.ParamName);
}
[Fact]
public async void ResolveServiceInstance_ResolvesAndIncrementsServiceIndex()
{
// arrange
var services = new List<ConfigurationServiceInstance>
{
new ConfigurationServiceInstance { ServiceId = "fruitservice", Host = "fruitball", Port = 8000, IsSecure = true },
new ConfigurationServiceInstance { ServiceId = "fruitservice", Host = "fruitballer", Port = 8001 },
new ConfigurationServiceInstance { ServiceId = "fruitservice", Host = "fruitballerz", Port = 8002 },
new ConfigurationServiceInstance { ServiceId = "vegetableservice", Host = "vegemite", Port = 8010, IsSecure = true },
new ConfigurationServiceInstance { ServiceId = "vegetableservice", Host = "carrot", Port = 8011 },
new ConfigurationServiceInstance { ServiceId = "vegetableservice", Host = "beet", Port = 8012 },
};
var serviceOptions = new TestOptionsMonitor<List<ConfigurationServiceInstance>>(services);
var provider = new ConfigurationServiceInstanceProvider(serviceOptions);
var loadBalancer = new RoundRobinLoadBalancer(provider);
// act
Assert.Throws<KeyNotFoundException>(() => loadBalancer.NextIndexForService[loadBalancer.IndexKeyPrefix + "fruitService"]);
Assert.Throws<KeyNotFoundException>(() => loadBalancer.NextIndexForService[loadBalancer.IndexKeyPrefix + "vegetableService"]);
var fruitResult = await loadBalancer.ResolveServiceInstanceAsync(new Uri("http://fruitservice/api"));
await loadBalancer.ResolveServiceInstanceAsync(new Uri("http://vegetableservice/api"));
var vegResult = await loadBalancer.ResolveServiceInstanceAsync(new Uri("http://vegetableservice/api"));
// assert
Assert.Equal(1, loadBalancer.NextIndexForService[loadBalancer.IndexKeyPrefix + "fruitservice"]);
Assert.Equal(8000, fruitResult.Port);
Assert.Equal(2, loadBalancer.NextIndexForService[loadBalancer.IndexKeyPrefix + "vegetableservice"]);
Assert.Equal(8011, vegResult.Port);
}
[Fact]
public async void ResolveServiceInstance_ResolvesAndIncrementsServiceIndex_WithDistributedCache()
{
// arrange
var services = new List<ConfigurationServiceInstance>
{
new ConfigurationServiceInstance { ServiceId = "fruitservice", Host = "fruitball", Port = 8000, IsSecure = true },
new ConfigurationServiceInstance { ServiceId = "fruitservice", Host = "fruitballer", Port = 8001 },
new ConfigurationServiceInstance { ServiceId = "fruitservice", Host = "fruitballerz", Port = 8002 },
new ConfigurationServiceInstance { ServiceId = "vegetableservice", Host = "vegemite", Port = 8010, IsSecure = true },
new ConfigurationServiceInstance { ServiceId = "vegetableservice", Host = "carrot", Port = 8011 },
new ConfigurationServiceInstance { ServiceId = "vegetableservice", Host = "beet", Port = 8012 },
};
var serviceOptions = new TestOptionsMonitor<List<ConfigurationServiceInstance>>(services);
var provider = new ConfigurationServiceInstanceProvider(serviceOptions);
var loadBalancer = new RoundRobinLoadBalancer(provider, GetCache());
// act
var fruitResult = await loadBalancer.ResolveServiceInstanceAsync(new Uri("http://fruitservice/api"));
await loadBalancer.ResolveServiceInstanceAsync(new Uri("http://vegetableservice/api"));
var vegResult = await loadBalancer.ResolveServiceInstanceAsync(new Uri("http://vegetableservice/api"));
// assert
Assert.Equal(8000, fruitResult.Port);
Assert.Equal(8011, vegResult.Port);
}
private IDistributedCache GetCache()
{
var services = new ServiceCollection();
services.AddDistributedMemoryCache();
var serviceProvider = services.BuildServiceProvider();
return serviceProvider.GetService<IDistributedCache>();
}
}
}

Просмотреть файл

@ -23,7 +23,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(ExtensionsVersion)" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(AspNetCoreTestVersion)" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(ExtensionsVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
<PackageReference Include="xunit" Version="$(XunitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitStudioVersion)" />

Просмотреть файл

@ -0,0 +1,47 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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.IO;
namespace Steeltoe.Common
{
public class TestHelpers
{
public static string CreateTempFile(string contents)
{
var tempFile = Path.GetTempFileName();
File.WriteAllText(tempFile, contents);
return tempFile;
}
public static Stream StringToStream(string str)
{
var memStream = new MemoryStream();
var textWriter = new StreamWriter(memStream);
textWriter.Write(str);
textWriter.Flush();
memStream.Seek(0, SeekOrigin.Begin);
return memStream;
}
public static string StreamToString(Stream stream)
{
stream.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
}
}

Просмотреть файл

@ -0,0 +1,39 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 Microsoft.Extensions.Options;
using System;
namespace Steeltoe.Common
{
public class TestOptionsMonitor<T> : IOptionsMonitor<T>
{
public TestOptionsMonitor(T currentValue)
{
CurrentValue = currentValue;
}
public T Get(string name)
{
return CurrentValue;
}
public IDisposable OnChange(Action<T, string> listener)
{
throw new NotImplementedException();
}
public T CurrentValue { get; }
}
}