зеркало из https://github.com/SteeltoeOSS/Common.git
Merge branch 'dev'
This commit is contained in:
Коммит
ffeeccded1
|
@ -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; }
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче