diff --git a/playground/AWS/AWS.ServiceDefaults/Extensions.cs b/playground/AWS/AWS.ServiceDefaults/Extensions.cs index 65949135f6..675692cbc8 100644 --- a/playground/AWS/AWS.ServiceDefaults/Extensions.cs +++ b/playground/AWS/AWS.ServiceDefaults/Extensions.cs @@ -28,7 +28,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); // Uncomment the following to restrict the allowed schemes for service discovery. diff --git a/playground/Playground.ServiceDefaults/Extensions.cs b/playground/Playground.ServiceDefaults/Extensions.cs index 0a17073ea3..8a6a27b060 100644 --- a/playground/Playground.ServiceDefaults/Extensions.cs +++ b/playground/Playground.ServiceDefaults/Extensions.cs @@ -28,7 +28,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); // Uncomment the following to restrict the allowed schemes for service discovery. diff --git a/playground/TestShop/ServiceDefaults/Extensions.cs b/playground/TestShop/ServiceDefaults/Extensions.cs index 806cdc988a..755a9f7367 100644 --- a/playground/TestShop/ServiceDefaults/Extensions.cs +++ b/playground/TestShop/ServiceDefaults/Extensions.cs @@ -25,7 +25,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); // Uncomment the following to restrict the allowed schemes for service discovery. diff --git a/playground/orleans/OrleansServiceDefaults/Extensions.cs b/playground/orleans/OrleansServiceDefaults/Extensions.cs index 7b654807f2..77a97dabc3 100644 --- a/playground/orleans/OrleansServiceDefaults/Extensions.cs +++ b/playground/orleans/OrleansServiceDefaults/Extensions.cs @@ -25,7 +25,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); // Uncomment the following to restrict the allowed schemes for service discovery. diff --git a/playground/seq/Seq.ServiceDefaults/Extensions.cs b/playground/seq/Seq.ServiceDefaults/Extensions.cs index 0706f133be..42ff5356ef 100644 --- a/playground/seq/Seq.ServiceDefaults/Extensions.cs +++ b/playground/seq/Seq.ServiceDefaults/Extensions.cs @@ -29,7 +29,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); // Uncomment the following to restrict the allowed schemes for service discovery. diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs deleted file mode 100644 index 63dc3e11a3..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Represents a feature that reports the health of an endpoint, for use in triggering internal cache refresh and for use in load balancing. -/// -public interface IEndPointHealthFeature -{ - /// - /// Reports health of the endpoint, for use in triggering internal cache refresh and for use in load balancing. Can be a no-op. - /// - /// The response time of the endpoint. - /// An optional exception that occurred while checking the endpoint's health. - void ReportHealth(TimeSpan responseTime, Exception? exception); -} - diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs deleted file mode 100644 index 2610f13594..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Represents a feature that provides information about the current load of an endpoint. -/// -public interface IEndPointLoadFeature -{ - /// - /// Gets a comparable measure of the current load of the endpoint (e.g. queue length, concurrent requests, etc). - /// - public double CurrentLoad { get; } -} - diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs similarity index 70% rename from src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs rename to src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs index fff3c3fa3f..c748947237 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs @@ -1,7 +1,7 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery; /// /// Exposes the host name of the end point. diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs new file mode 100644 index 0000000000..468adea1c0 --- /dev/null +++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Builder to create a instances. +/// +public interface IServiceEndPointBuilder +{ + /// + /// Gets the endpoints. + /// + IList EndPoints { get; } + + /// + /// Gets the feature collection. + /// + IFeatureCollection Features { get; } + + /// + /// Adds a change token to the resulting . + /// + /// The change token. + void AddChangeToken(IChangeToken changeToken); +} diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs index 3b369a9785..950823257a 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery; /// /// Provides details about a service's endpoints. @@ -14,5 +14,5 @@ public interface IServiceEndPointProvider : IAsyncDisposable /// The endpoint collection, which resolved endpoints will be added to. /// The token to monitor for cancellation requests. /// The resolution status. - ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken); + ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken); } diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs similarity index 59% rename from src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs rename to src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs index 51343a5369..4b1876f808 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs @@ -3,18 +3,18 @@ using System.Diagnostics.CodeAnalysis; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery; /// /// Creates instances. /// -public interface IServiceEndPointResolverProvider +public interface IServiceEndPointProviderFactory { /// - /// Tries to create an instance for the specified . + /// Tries to create an instance for the specified . /// - /// The service to create the resolver for. + /// The service to create the resolver for. /// The resolver. /// if the resolver was created, otherwise. - bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver); + bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver); } diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs deleted file mode 100644 index 27f4ec4324..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Functionality for creating instances. -/// -public interface IServiceEndPointSelectorProvider -{ - /// - /// Creates an instance. - /// - /// A new instance. - IServiceEndPointSelector CreateSelector(); -} diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs index b73635ecd5..7d135dfe97 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs @@ -4,21 +4,11 @@ using System.Net; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.Internal; -internal sealed class ServiceEndPointImpl : ServiceEndPoint +internal sealed class ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndPoint { - private readonly IFeatureCollection _features; - private readonly EndPoint _endPoint; - - public ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = null) - { - _endPoint = endPoint; - _features = features ?? new FeatureCollection(); - } - - public override EndPoint EndPoint => _endPoint; - public override IFeatureCollection Features => _features; - + public override EndPoint EndPoint { get; } = endPoint; + public override IFeatureCollection Features { get; } = features ?? new FeatureCollection(); public override string? ToString() => GetEndPointString(); } diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs deleted file mode 100644 index 04eec95dc6..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Represents the status of an endpoint resolution operation. -/// -public readonly struct ResolutionStatus(ResolutionStatusCode statusCode, Exception? exception, string message) : IEquatable -{ - /// - /// Indicates that resolution was not performed. - /// - public static readonly ResolutionStatus None = new(ResolutionStatusCode.None, exception: null, message: ""); - - /// - /// Indicates that resolution is ongoing and has not yet completed. - /// - public static readonly ResolutionStatus Pending = new(ResolutionStatusCode.Pending, exception: null, message: "Pending"); - - /// - /// Indicates that resolution has completed successfully. - /// - public static readonly ResolutionStatus Success = new(ResolutionStatusCode.Success, exception: null, message: "Success"); - - /// - /// Indicates that resolution was cancelled. - /// - public static readonly ResolutionStatus Cancelled = new(ResolutionStatusCode.Cancelled, exception: null, message: "Cancelled"); - - /// - /// Indicates that resolution did not find a result for the service. - /// - public static ResolutionStatus CreateNotFound(string message) => new(ResolutionStatusCode.NotFound, exception: null, message: message); - - /// - /// Creates a status with a equal to with the provided exception. - /// - /// The resolution exception. - /// A new instance. - public static ResolutionStatus FromException(Exception exception) - { - ArgumentNullException.ThrowIfNull(exception); - return new ResolutionStatus(ResolutionStatusCode.Error, exception, exception.Message); - } - - /// - /// Creates a status with a equal to with the provided exception. - /// - /// The resolution exception, if there was one. - /// A new instance. - public static ResolutionStatus FromPending(Exception? exception = null) - { - ArgumentNullException.ThrowIfNull(exception); - return new ResolutionStatus(ResolutionStatusCode.Pending, exception, exception.Message); - } - - /// - /// Gets the resolution status code. - /// - public ResolutionStatusCode StatusCode { get; } = statusCode; - - /// - /// Gets the resolution exception. - /// - - public Exception? Exception { get; } = exception; - - /// - /// Gets the resolution status message. - /// - public string Message { get; } = message; - - /// - /// Compares the provided operands, returning if they are equal and if they are not equal. - /// - public static bool operator ==(ResolutionStatus left, ResolutionStatus right) => left.Equals(right); - - /// - /// Compares the provided operands, returning if they are not equal and if they are equal. - /// - public static bool operator !=(ResolutionStatus left, ResolutionStatus right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is ResolutionStatus status && Equals(status); - - /// - public bool Equals(ResolutionStatus other) => StatusCode == other.StatusCode && - EqualityComparer.Default.Equals(Exception, other.Exception) && - Message == other.Message; - - /// - public override int GetHashCode() => HashCode.Combine(StatusCode, Exception, Message); - - /// - public override string ToString() => Exception switch - { - not null => $"[{nameof(StatusCode)}: {StatusCode}, {nameof(Message)}: {Message}, {nameof(Exception)}: {Exception}]", - _ => $"[{nameof(StatusCode)}: {StatusCode}, {nameof(Message)}: {Message}]" - }; -} diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs deleted file mode 100644 index 7157eac758..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Status codes for . -/// -public enum ResolutionStatusCode -{ - /// - /// Resolution has not been performed. - /// - None = 0, - - /// - /// Resolution is pending completion. - /// - Pending = 1, - - /// - /// Resolution did not find any end points for the specified service. - /// - NotFound = 2, - - /// - /// Resolution was successful. - /// - Success = 3, - - /// - /// Resolution was canceled. - /// - Cancelled = 4, - - /// - /// Resolution failed. - /// - Error = 5, -} diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs index 9dc4675dad..a3cde62ce0 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs @@ -4,8 +4,9 @@ using System.Diagnostics; using System.Net; using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery; /// /// Represents an endpoint for a service. diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs deleted file mode 100644 index 94f274a38e..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Http.Features; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// A mutable collection of service endpoints. -/// -public class ServiceEndPointCollectionSource(string serviceName, IFeatureCollection features) -{ - private readonly List _endPoints = new(); - private readonly List _changeTokens = new(); - - /// - /// Gets the service name. - /// - public string ServiceName { get; } = serviceName; - - /// - /// Adds a change token. - /// - /// The change token. - public void AddChangeToken(IChangeToken changeToken) - { - _changeTokens.Add(changeToken); - } - - /// - /// Gets the composite change token. - /// - /// The composite change token. - public IChangeToken GetChangeToken() => new CompositeChangeToken(_changeTokens); - - /// - /// Gets the feature collection. - /// - public IFeatureCollection Features { get; } = features; - - /// - /// Gets the endpoints. - /// - public IList EndPoints => _endPoints; - - /// - /// Creates a from the provided instance. - /// - /// The source collection. - /// The service endpoint collection. - public static ServiceEndPointCollection CreateServiceEndPointCollection(ServiceEndPointCollectionSource source) - { - return new ServiceEndPointCollection(source.ServiceName, source._endPoints, source.GetChangeToken(), source.Features); - } -} - diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs new file mode 100644 index 0000000000..99c92cce27 --- /dev/null +++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Describes a query for endpoints of a service. +/// +public sealed class ServiceEndPointQuery +{ + /// + /// Initializes a new instance. + /// + /// The string which the query was constructed from. + /// The ordered list of included URI schemes. + /// The service name. + /// The optional endpoint name. + private ServiceEndPointQuery(string originalString, string[] includedSchemes, string serviceName, string? endPointName) + { + OriginalString = originalString; + IncludeSchemes = includedSchemes; + ServiceName = serviceName; + EndPointName = endPointName; + } + + /// + /// Tries to parse the provided input as a service endpoint query. + /// + /// The value to parse. + /// The resulting query. + /// if the value was successfully parsed; otherwise . + public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPointQuery? query) + { + bool hasScheme; + if (!input.Contains("://", StringComparison.InvariantCulture) + && Uri.TryCreate($"fakescheme://{input}", default, out var uri)) + { + hasScheme = false; + } + else if (Uri.TryCreate(input, default, out uri)) + { + hasScheme = true; + } + else + { + query = null; + return false; + } + + var uriHost = uri.Host; + var segmentSeparatorIndex = uriHost.IndexOf('.'); + string host; + string? endPointName = null; + if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') + { + endPointName = uriHost[1..segmentSeparatorIndex]; + + // Skip the endpoint name, including its prefix ('_') and suffix ('.'). + host = uriHost[(segmentSeparatorIndex + 1)..]; + } + else + { + host = uriHost; + } + + // Allow multiple schemes to be separated by a '+', eg. "https+http://host:port". + var schemes = hasScheme ? uri.Scheme.Split('+') : []; + query = new(input, schemes, host, endPointName); + return true; + } + + /// + /// Gets the string which the query was constructed from. + /// + public string OriginalString { get; } + + /// + /// Gets the ordered list of included URI schemes. + /// + public IReadOnlyList IncludeSchemes { get; } + + /// + /// Gets the endpoint name, or if no endpoint name is specified. + /// + public string? EndPointName { get; } + + /// + /// Gets the service name. + /// + public string ServiceName { get; } + + /// + public override string? ToString() => EndPointName is not null ? $"Service: {ServiceName}, Endpoint: {EndPointName}, Schemes: {string.Join(", ", IncludeSchemes)}" : $"Service: {ServiceName}, Schemes: {string.Join(", ", IncludeSchemes)}"; +} + diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs deleted file mode 100644 index 9179ed2f11..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Represents the result of service endpoint resolution. -/// -/// The endpoint collection. -/// The status. -public sealed class ServiceEndPointResolverResult(ServiceEndPointCollection? endPoints, ResolutionStatus status) -{ - /// - /// Gets the status. - /// - public ResolutionStatus Status { get; } = status; - - /// - /// Gets a value indicating whether resolution completed successfully. - /// - [MemberNotNullWhen(true, nameof(EndPoints))] - public bool ResolvedSuccessfully => Status.StatusCode is ResolutionStatusCode.Success; - - /// - /// Gets the endpoints. - /// - public ServiceEndPointCollection? EndPoints { get; } = endPoints; -} diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs similarity index 55% rename from src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs rename to src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs index c9540f2e7a..807981226e 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs @@ -1,47 +1,40 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections; using System.Diagnostics; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Represents an immutable collection of service endpoints. +/// Represents a collection of service endpoints. /// [DebuggerDisplay("{ToString(),nq}")] [DebuggerTypeProxy(typeof(ServiceEndPointCollectionDebuggerView))] -public class ServiceEndPointCollection : IReadOnlyList +public sealed class ServiceEndPointSource { private readonly List? _endpoints; /// - /// Initializes a new instance. + /// Initializes a new instance. /// - /// The service name. /// The endpoints. /// The change token. /// The feature collection. - public ServiceEndPointCollection(string serviceName, List? endpoints, IChangeToken changeToken, IFeatureCollection features) + public ServiceEndPointSource(List? endpoints, IChangeToken changeToken, IFeatureCollection features) { - ArgumentNullException.ThrowIfNull(serviceName); ArgumentNullException.ThrowIfNull(changeToken); _endpoints = endpoints; Features = features; - ServiceName = serviceName; ChangeToken = changeToken; } - /// - public ServiceEndPoint this[int index] => _endpoints?[index] ?? throw new ArgumentOutOfRangeException(nameof(index)); - /// - /// Gets the service name. + /// Gets the endpoints. /// - public string ServiceName { get; } + public IReadOnlyList EndPoints => _endpoints ?? (IReadOnlyList)[]; /// /// Gets the change token which indicates when this collection should be refreshed. @@ -53,15 +46,6 @@ public ServiceEndPointCollection(string serviceName, List? endp /// public IFeatureCollection Features { get; } - /// - public int Count => _endpoints?.Count ?? 0; - - /// - public IEnumerator GetEnumerator() => _endpoints?.GetEnumerator() ?? Enumerable.Empty().GetEnumerator(); - - /// - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - /// public override string ToString() { @@ -73,15 +57,13 @@ public override string ToString() return $"[{string.Join(", ", eps)}]"; } - private sealed class ServiceEndPointCollectionDebuggerView(ServiceEndPointCollection value) + private sealed class ServiceEndPointCollectionDebuggerView(ServiceEndPointSource value) { - public string ServiceName => value.ServiceName; - public IChangeToken ChangeToken => value.ChangeToken; public IFeatureCollection Features => value.Features; [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public ServiceEndPoint[] EndPoints => value.ToArray(); + public ServiceEndPoint[] EndPoints => value.EndPoints.ToArray(); } } diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 4a8350483e..a2601c84b4 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -4,7 +4,6 @@ using System.Net; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -45,8 +44,7 @@ protected override async Task ResolveAsyncCore() if (endPoints.Count == 0) { - SetException(new InvalidOperationException($"No DNS records were found for service {ServiceName} (DNS name: {hostName}).")); - return; + throw new InvalidOperationException($"No DNS records were found for service {ServiceName} (DNS name: {hostName})."); } SetResult(endPoints, ttl); diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs index 516c8ed1f6..9d6c54e475 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -18,7 +17,7 @@ internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPoin private readonly TimeProvider _timeProvider; private long _lastRefreshTimeStamp; private Task _resolveTask = Task.CompletedTask; - private ResolutionStatus _lastStatus; + private bool _hasEndpoints; private CancellationChangeToken _lastChangeToken; private CancellationTokenSource _lastCollectionCancellation; private List? _lastEndPointCollection; @@ -59,13 +58,13 @@ protected DnsServiceEndPointResolverBase( protected CancellationToken ShutdownToken => _disposeCancellation.Token; /// - public async ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) + public async ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) { // Only add endpoints to the collection if a previous provider (eg, a configuration override) did not add them. if (endPoints.EndPoints.Count != 0) { Log.SkippedResolution(_logger, ServiceName, "Collection has existing endpoints"); - return ResolutionStatus.None; + return; } if (ShouldRefresh()) @@ -75,7 +74,7 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS { if (_resolveTask.IsCompleted && ShouldRefresh()) { - _resolveTask = ResolveAsyncInternal(); + _resolveTask = ResolveAsyncCore(); } resolveTask = _resolveTask; @@ -95,7 +94,7 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS } endPoints.AddChangeToken(_lastChangeToken); - return _lastStatus; + return; } } @@ -103,53 +102,21 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS protected abstract Task ResolveAsyncCore(); - private async Task ResolveAsyncInternal() - { - try - { - await ResolveAsyncCore().ConfigureAwait(false); - } - catch (Exception exception) - { - SetException(exception); - throw; - } - - } - - protected void SetException(Exception exception) => SetResult(endPoints: null, exception, validityPeriod: TimeSpan.Zero); - - protected void SetResult(List endPoints, TimeSpan validityPeriod) => SetResult(endPoints, exception: null, validityPeriod); - - private void SetResult(List? endPoints, Exception? exception, TimeSpan validityPeriod) + protected void SetResult(List endPoints, TimeSpan validityPeriod) { lock (_lock) { - if (exception is not null) + if (endPoints is { Count: > 0 }) { - _nextRefreshPeriod = GetRefreshPeriod(); - if (_lastEndPointCollection is null) - { - // Since end points have never been resolved, use a pending status to indicate that they might appear - // soon and to retry for some period until they do. - _lastStatus = ResolutionStatus.FromPending(exception); - } - else - { - _lastStatus = ResolutionStatus.FromException(exception); - } + _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); + _nextRefreshPeriod = DefaultRefreshPeriod; + _hasEndpoints = true; } - else if (endPoints is not { Count: > 0 }) + else { _nextRefreshPeriod = GetRefreshPeriod(); validityPeriod = TimeSpan.Zero; - _lastStatus = ResolutionStatus.Pending; - } - else - { - _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); - _nextRefreshPeriod = DefaultRefreshPeriod; - _lastStatus = ResolutionStatus.Success; + _hasEndpoints = false; } if (validityPeriod <= TimeSpan.Zero) @@ -169,13 +136,18 @@ private void SetResult(List? endPoints, Exception? exception, T TimeSpan GetRefreshPeriod() { - if (_lastStatus.StatusCode is ResolutionStatusCode.Success) + if (_hasEndpoints) { return MinRetryPeriod; } - var nextPeriod = TimeSpan.FromTicks((long)(_nextRefreshPeriod.Ticks * RetryBackOffFactor)); - return nextPeriod > MaxRetryPeriod ? MaxRetryPeriod : nextPeriod; + var nextTicks = (long)(_nextRefreshPeriod.Ticks * RetryBackOffFactor); + if (nextTicks <= 0 || nextTicks > MaxRetryPeriod.Ticks) + { + return MaxRetryPeriod; + } + + return TimeSpan.FromTicks(nextTicks); } } diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs index 37879b5f3e..665c98bbc0 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.ServiceDiscovery.Abstractions; - namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs index 8f676327d5..51525663a0 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs @@ -4,28 +4,18 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery.Dns; internal sealed partial class DnsServiceEndPointResolverProvider( IOptionsMonitor options, ILogger logger, - TimeProvider timeProvider, - ServiceNameParser parser) : IServiceEndPointResolverProvider + TimeProvider timeProvider) : IServiceEndPointProviderFactory { /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { - if (!parser.TryParse(serviceName, out var parts)) - { - DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName); - resolver = default; - return false; - } - - resolver = new DnsServiceEndPointResolver(serviceName, hostName: parts.Host, options, logger, timeProvider); + resolver = new DnsServiceEndPointResolver(query.OriginalString, hostName: query.ServiceName, options, logger, timeProvider); return true; } } diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index 97a0d47d02..d59dbfbb69 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -6,7 +6,6 @@ using DnsClient.Protocol; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -39,8 +38,7 @@ protected override async Task ResolveAsyncCore() var result = await dnsClient.QueryAsync(srvQuery, QueryType.SRV, cancellationToken: ShutdownToken).ConfigureAwait(false); if (result.HasError) { - SetException(CreateException(srvQuery, result.ErrorMessage)); - return; + throw CreateException(srvQuery, result.ErrorMessage); } var lookupMapping = new Dictionary(); diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs index 5bac96c6c0..704e03cd9c 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.ServiceDiscovery.Abstractions; - namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs index d02dcfb727..8a75c1d1bb 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs @@ -5,8 +5,6 @@ using DnsClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -14,8 +12,7 @@ internal sealed partial class DnsSrvServiceEndPointResolverProvider( IOptionsMonitor options, ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider, - ServiceNameParser parser) : IServiceEndPointResolverProvider + TimeProvider timeProvider) : IServiceEndPointProviderFactory { private static readonly string s_serviceAccountPath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount"); private static readonly string s_serviceAccountNamespacePath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace"); @@ -23,7 +20,7 @@ internal sealed partial class DnsSrvServiceEndPointResolverProvider( private readonly string? _querySuffix = options.CurrentValue.QuerySuffix ?? GetKubernetesHostDomain(); /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { // If a default namespace is not specified, then this provider will attempt to infer the namespace from the service name, but only when running inside Kubernetes. // Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md @@ -33,6 +30,7 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi // Otherwise, the namespace can be read from /var/run/secrets/kubernetes.io/serviceaccount/namespace and combined with an assumed suffix of "svc.cluster.local". // The protocol is assumed to be "tcp". // The portName is the name of the port in the service definition. If the serviceName parses as a URI, we use the scheme as the port name, otherwise "default". + var serviceName = query.OriginalString; if (string.IsNullOrWhiteSpace(_querySuffix)) { DnsServiceEndPointResolverBase.Log.NoDnsSuffixFound(logger, serviceName); @@ -40,16 +38,9 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi return false; } - if (!parser.TryParse(serviceName, out var parts)) - { - DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName); - resolver = default; - return false; - } - - var portName = parts.EndPointName ?? "default"; - var srvQuery = $"_{portName}._tcp.{parts.Host}.{_querySuffix}"; - resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, hostName: parts.Host, options, logger, dnsClient, timeProvider); + var portName = query.EndPointName ?? "default"; + var srvQuery = $"_{portName}._tcp.{query.ServiceName}.{_querySuffix}"; + resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, hostName: query.ServiceName, options, logger, dnsClient, timeProvider); return true; } diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs similarity index 88% rename from src/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs rename to src/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index e385bde69a..0d795660fd 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs @@ -4,7 +4,7 @@ using DnsClient; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Dns; namespace Microsoft.Extensions.Hosting; @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.Hosting; /// /// Extensions for to add service discovery. /// -public static class HostingExtensions +public static class ServiceDiscoveryDnsServiceCollectionExtensions { /// /// Adds DNS SRV service discovery to the . @@ -28,7 +28,7 @@ public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceC { services.AddServiceDiscoveryCore(); services.TryAddSingleton(); - services.AddSingleton(); + services.AddSingleton(); var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; @@ -46,7 +46,7 @@ public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceC public static IServiceCollection AddDnsServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); + services.AddSingleton(); var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index e35be5d629..22d7e6d832 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -54,32 +54,32 @@ public async ValueTask ResolveDestinationsAsync(I var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority; var serviceName = originalUri.GetLeftPart(UriPartial.Authority); - var endPoints = await resolver.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); - var results = new List<(string Name, DestinationConfig Config)>(endPoints.Count); + var result = await resolver.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); + var results = new List<(string Name, DestinationConfig Config)>(result.EndPoints.Count); var uriBuilder = new UriBuilder(originalUri); var healthUri = originalConfig.Health is { Length: > 0 } health ? new Uri(health) : null; var healthUriBuilder = healthUri is { } ? new UriBuilder(healthUri) : null; - foreach (var endPoint in endPoints) + foreach (var endPoint in result.EndPoints) { var addressString = endPoint.GetEndPointString(); - Uri result; + Uri uri; if (!addressString.Contains("://")) { - result = new Uri($"https://{addressString}"); + uri = new Uri($"https://{addressString}"); } else { - result = new Uri(addressString); + uri = new Uri(addressString); } - uriBuilder.Host = result.Host; - uriBuilder.Port = result.Port; + uriBuilder.Host = uri.Host; + uriBuilder.Port = uri.Port; var resolvedAddress = uriBuilder.Uri.ToString(); var healthAddress = originalConfig.Health; if (healthUriBuilder is not null) { - healthUriBuilder.Host = result.Host; - healthUriBuilder.Port = result.Port; + healthUriBuilder.Host = uri.Host; + healthUriBuilder.Port = uri.Port; healthAddress = healthUriBuilder.Uri.ToString(); } @@ -88,6 +88,6 @@ public async ValueTask ResolveDestinationsAsync(I results.Add((name, config)); } - return (results, endPoints.ChangeToken); + return (results, result.ChangeToken); } } diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs b/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs index d37e4f1407..84aafe2a67 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs @@ -1,22 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.Http; using Yarp.ReverseProxy.Forwarder; namespace Microsoft.Extensions.ServiceDiscovery.Yarp; -internal sealed class ServiceDiscoveryForwarderHttpClientFactory( - TimeProvider timeProvider, - IServiceEndPointSelectorProvider selectorProvider, - ServiceEndPointResolverFactory factory, - IOptions options) : ForwarderHttpClientFactory +internal sealed class ServiceDiscoveryForwarderHttpClientFactory(IServiceDiscoveryHttpMessageHandlerFactory handlerFactory) + : ForwarderHttpClientFactory { protected override HttpMessageHandler WrapHandler(ForwarderHttpClientContext context, HttpMessageHandler handler) { - var registry = new HttpServiceEndPointResolver(factory, selectorProvider, timeProvider); - return new ResolvingHttpDelegatingHandler(registry, options, handler); + return handlerFactory.CreateHandler(handler); } } diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs b/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs similarity index 94% rename from src/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs rename to src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs index e52ff65ee2..9f473fd3a9 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.ServiceDiscovery.Yarp; using Yarp.ReverseProxy.Forwarder; using Yarp.ReverseProxy.ServiceDiscovery; @@ -11,7 +10,7 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// Extensions for used to register the ReverseProxy's components. /// -public static class ReverseProxyServiceCollectionExtensions +public static class ServiceDiscoveryReverseProxyServiceCollectionExtensions { /// /// Provides a implementation which uses service discovery to resolve destinations. diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs index 5916951c69..fdb61ef59f 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs @@ -1,12 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Globalization; using System.Text; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.Configuration; internal sealed partial class ConfigurationServiceEndPointResolver { @@ -15,38 +13,16 @@ private sealed partial class Log [LoggerMessage(1, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")] public static partial void SkippedResolution(ILogger logger, string serviceName, string reason); - [LoggerMessage(2, LogLevel.Debug, "Matching endpoints using endpoint names for service '{ServiceName}' since endpoint names are specified in configuration.", EventName = "MatchingEndPointNames")] - public static partial void MatchingEndPointNames(ILogger logger, string serviceName); - - [LoggerMessage(3, LogLevel.Debug, "Ignoring endpoints using endpoint names for service '{ServiceName}' since no endpoint names are specified in configuration.", EventName = "IgnoringEndPointNames")] - public static partial void IgnoringEndPointNames(ILogger logger, string serviceName); - - public static void EndPointNameMatchSelection(ILogger logger, string serviceName, bool matchEndPointNames) - { - if (!logger.IsEnabled(LogLevel.Debug)) - { - return; - } - - if (matchEndPointNames) - { - MatchingEndPointNames(logger, serviceName); - } - else - { - IgnoringEndPointNames(logger, serviceName); - } - } - - [LoggerMessage(4, LogLevel.Debug, "Using configuration from path '{Path}' to resolve endpoint '{EndpointName}' for service '{ServiceName}'.", EventName = "UsingConfigurationPath")] + [LoggerMessage(2, LogLevel.Debug, "Using configuration from path '{Path}' to resolve endpoint '{EndpointName}' for service '{ServiceName}'.", EventName = "UsingConfigurationPath")] public static partial void UsingConfigurationPath(ILogger logger, string path, string endpointName, string serviceName); - [LoggerMessage(5, LogLevel.Debug, "No valid endpoint configuration was found for service '{ServiceName}' from path '{Path}'.", EventName = "ServiceConfigurationNotFound")] + [LoggerMessage(3, LogLevel.Debug, "No valid endpoint configuration was found for service '{ServiceName}' from path '{Path}'.", EventName = "ServiceConfigurationNotFound")] internal static partial void ServiceConfigurationNotFound(ILogger logger, string serviceName, string path); - [LoggerMessage(6, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndPoints}.", EventName = "ConfiguredEndPoints")] + [LoggerMessage(4, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndPoints}.", EventName = "ConfiguredEndPoints")] internal static partial void ConfiguredEndPoints(ILogger logger, string serviceName, string path, string configuredEndPoints); - public static void ConfiguredEndPoints(ILogger logger, string serviceName, string path, List parsedValues) + + internal static void ConfiguredEndPoints(ILogger logger, string serviceName, string path, IList endpoints, int added) { if (!logger.IsEnabled(LogLevel.Debug)) { @@ -54,21 +30,21 @@ public static void ConfiguredEndPoints(ILogger logger, string serviceName, strin } StringBuilder endpointValues = new(); - for (var i = 0; i < parsedValues.Count; i++) + for (var i = endpoints.Count - added; i < endpoints.Count; i++) { if (endpointValues.Length > 0) { endpointValues.Append(", "); } - endpointValues.Append(CultureInfo.InvariantCulture, $"({parsedValues[i]})"); + endpointValues.Append(endpoints[i].ToString()); } var configuredEndPoints = endpointValues.ToString(); ConfiguredEndPoints(logger, serviceName, path, configuredEndPoints); } - [LoggerMessage(7, LogLevel.Debug, "No valid endpoint configuration was found for endpoint '{EndpointName}' on service '{ServiceName}' from path '{Path}'.", EventName = "EndpointConfigurationNotFound")] + [LoggerMessage(5, LogLevel.Debug, "No valid endpoint configuration was found for endpoint '{EndpointName}' on service '{ServiceName}' from path '{Path}'.", EventName = "EndpointConfigurationNotFound")] internal static partial void EndpointConfigurationNotFound(ILogger logger, string endpointName, string serviceName, string path); } } diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index dae054c988..9604ec0c20 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -6,9 +6,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.Configuration; /// /// A service endpoint resolver that uses configuration to resolve resolved. @@ -26,29 +25,21 @@ internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEnd /// /// Initializes a new instance. /// - /// The service name. + /// The query. /// The configuration. /// The logger. - /// The options. - /// The service name parser. + /// Configuration resolver options. + /// Service discovery options. public ConfigurationServiceEndPointResolver( - string serviceName, + ServiceEndPointQuery query, IConfiguration configuration, ILogger logger, IOptions options, - ServiceNameParser parser) + IOptions serviceDiscoveryOptions) { - if (parser.TryParse(serviceName, out var parts)) - { - _serviceName = parts.Host; - _endpointName = parts.EndPointName; - _schemes = parts.Schemes; - } - else - { - throw new InvalidOperationException($"Service name '{serviceName}' is not valid."); - } - + _serviceName = query.ServiceName; + _endpointName = query.EndPointName; + _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludeSchemes, serviceDiscoveryOptions.Value.AllowedSchemes); _configuration = configuration; _logger = logger; _options = options; @@ -58,24 +49,22 @@ public ConfigurationServiceEndPointResolver( public ValueTask DisposeAsync() => default; /// - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => new(ResolveInternal(endPoints)); - - string IHostNameFeature.HostName => _serviceName; - - private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoints) + public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) { // Only add resolved to the collection if a previous provider (eg, an override) did not add them. if (endPoints.EndPoints.Count != 0) { Log.SkippedResolution(_logger, _serviceName, "Collection has existing endpoints"); - return ResolutionStatus.None; + return default; } // Get the corresponding config section. var section = _configuration.GetSection(_options.Value.SectionName).GetSection(_serviceName); if (!section.Exists()) { - return CreateNotFoundResponse(endPoints, $"{_options.Value.SectionName}:{_serviceName}"); + endPoints.AddChangeToken(_configuration.GetReloadToken()); + Log.ServiceConfigurationNotFound(_logger, _serviceName, $"{_options.Value.SectionName}:{_serviceName}"); + return default; } endPoints.AddChangeToken(section.GetReloadToken()); @@ -119,7 +108,8 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin var configPath = $"{_options.Value.SectionName}:{_serviceName}:{endpointName}"; if (!namedSection.Exists()) { - return CreateNotFoundResponse(endPoints, configPath); + Log.EndpointConfigurationNotFound(_logger, endpointName, _serviceName, configPath); + return default; } List resolved = []; @@ -129,10 +119,7 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin if (!string.IsNullOrWhiteSpace(namedSection.Value)) { // Single value case. - if (!TryAddEndPoint(resolved, namedSection, endpointName, out var error)) - { - return error; - } + AddEndPoint(resolved, namedSection, endpointName); } else { @@ -141,13 +128,10 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin { if (!int.TryParse(child.Key, out _)) { - return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has non-numeric keys.")); + throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has non-numeric keys."); } - if (!TryAddEndPoint(resolved, child, endpointName, out var error)) - { - return error; - } + AddEndPoint(resolved, child, endpointName); } } @@ -186,25 +170,27 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin if (added == 0) { - return CreateNotFoundResponse(endPoints, configPath); + Log.ServiceConfigurationNotFound(_logger, _serviceName, configPath); + } + else + { + Log.ConfiguredEndPoints(_logger, _serviceName, configPath, endPoints.EndPoints, added); } - return ResolutionStatus.Success; - + return default; } - private bool TryAddEndPoint(List endPoints, IConfigurationSection section, string endpointName, out ResolutionStatus error) + string IHostNameFeature.HostName => _serviceName; + + private void AddEndPoint(List endPoints, IConfigurationSection section, string endpointName) { var value = section.Value; if (string.IsNullOrWhiteSpace(value) || !TryParseEndPoint(value, out var endPoint)) { - error = ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'.")); - return false; + throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'."); } endPoints.Add(CreateEndPoint(endPoint)); - error = default; - return true; } private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPoint? endPoint) @@ -246,12 +232,5 @@ private ServiceEndPoint CreateEndPoint(EndPoint endPoint) return serviceEndPoint; } - private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string configPath) - { - endPoints.AddChangeToken(_configuration.GetReloadToken()); - Log.ServiceConfigurationNotFound(_logger, _serviceName, configPath); - return ResolutionStatus.CreateNotFound($"No valid endpoint configuration was found for service '{_serviceName}' from path '{configPath}'."); - } - public override string ToString() => "Configuration"; } diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs similarity index 50% rename from src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs rename to src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs index c83589eb26..91e97b5d0b 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs @@ -1,25 +1,9 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Options; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Options for . -/// -public sealed class ConfigurationServiceEndPointResolverOptions -{ - /// - /// The name of the configuration section which contains service endpoints. Defaults to "Services". - /// - public string SectionName { get; set; } = "Services"; - - /// - /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to false. - /// - public Func ApplyHostNameMetadata { get; set; } = _ => false; -} +namespace Microsoft.Extensions.ServiceDiscovery.Configuration; internal sealed class ConfigurationServiceEndPointResolverOptionsValidator : IValidateOptions { diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs index 472205f12f..032c50b6f2 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs @@ -5,23 +5,22 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.Configuration; /// -/// implementation that resolves services using . +/// implementation that resolves services using . /// internal sealed class ConfigurationServiceEndPointResolverProvider( IConfiguration configuration, IOptions options, - ILogger logger, - ServiceNameParser parser) : IServiceEndPointResolverProvider + IOptions serviceDiscoveryOptions, + ILogger logger) : IServiceEndPointProviderFactory { /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { - resolver = new ConfigurationServiceEndPointResolver(serviceName, configuration, logger, options, parser); + resolver = new ConfigurationServiceEndPointResolver(query, configuration, logger, options, serviceDiscoveryOptions); return true; } } diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs b/src/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs new file mode 100644 index 0000000000..d3b94f2f1e --- /dev/null +++ b/src/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.ServiceDiscovery.Configuration; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Options for . +/// +public sealed class ConfigurationServiceEndPointResolverOptions +{ + /// + /// The name of the configuration section which contains service endpoints. Defaults to "Services". + /// + public string SectionName { get; set; } = "Services"; + + /// + /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to a delegate which returns false. + /// + public Func ApplyHostNameMetadata { get; set; } = _ => false; +} diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs index b4f6249f28..44e58b0dbb 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs @@ -3,21 +3,21 @@ using System.Collections.Concurrent; using System.Diagnostics; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ServiceDiscovery.LoadBalancing; namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// Resolves endpoints for HTTP requests. /// -public class HttpServiceEndPointResolver(ServiceEndPointResolverFactory resolverFactory, IServiceEndPointSelectorProvider selectorProvider, TimeProvider timeProvider) : IAsyncDisposable +internal sealed class HttpServiceEndPointResolver(ServiceEndPointWatcherFactory resolverFactory, IServiceProvider serviceProvider, TimeProvider timeProvider) : IAsyncDisposable { private static readonly TimerCallback s_cleanupCallback = s => ((HttpServiceEndPointResolver)s!).CleanupResolvers(); private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); private readonly object _lock = new(); - private readonly ServiceEndPointResolverFactory _resolverFactory = resolverFactory; - private readonly IServiceEndPointSelectorProvider _selectorProvider = selectorProvider; + private readonly ServiceEndPointWatcherFactory _resolverFactory = resolverFactory; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; private Task? _cleanupTask; @@ -148,8 +148,8 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverFactory.CreateResolver(serviceName); - var selector = _selectorProvider.CreateSelector(); + var resolver = _resolverFactory.CreateWatcher(serviceName); + var selector = serviceProvider.GetService() ?? new RoundRobinServiceEndPointSelector(); var result = new ResolverEntry(resolver, selector); resolver.Start(); return result; @@ -173,7 +173,7 @@ public ResolverEntry(ServiceEndPointWatcher resolver, IServiceEndPointSelector s { if (result.ResolvedSuccessfully) { - _selector.SetEndPoints(result.EndPoints); + _selector.SetEndPoints(result.EndPointSource); } }; } diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs new file mode 100644 index 0000000000..0febfa9481 --- /dev/null +++ b/src/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.ServiceDiscovery.Http; + +/// +/// Factory which creates instances which resolve endpoints using service discovery +/// before delegating to a provided handler. +/// +public interface IServiceDiscoveryHttpMessageHandlerFactory +{ + /// + /// Creates an instance which resolve endpoints using service discovery before + /// delegating to a provided handler. + /// + /// The handler to delegate to. + /// The new . + HttpMessageHandler CreateHandler(HttpMessageHandler handler); +} diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index 39eb65cc18..bc06a03170 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -1,16 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// which resolves endpoints using service discovery. /// -public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IOptions options) : HttpClientHandler +internal sealed class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IOptions options) : HttpClientHandler { private readonly HttpServiceEndPointResolver _resolver = resolver; private readonly ServiceDiscoveryOptions _options = options.Value; @@ -19,29 +17,20 @@ public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IO protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var originalUri = request.RequestUri; - IEndPointHealthFeature? epHealth = null; - Exception? error = null; - var startTime = Stopwatch.GetTimestamp(); - if (originalUri?.Host is not null) - { - var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result, _options); - request.Headers.Host ??= result.Features.Get()?.HostName; - epHealth = result.Features.Get(); - } try { + if (originalUri?.Host is not null) + { + var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); + request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result, _options); + request.Headers.Host ??= result.Features.Get()?.HostName; + } + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } - catch (Exception exception) - { - error = exception; - throw; - } finally { - epHealth?.ReportHealth(Stopwatch.GetElapsedTime(startTime), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. request.RequestUri = originalUri; } } diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index 976bfb331e..daa7b8a17d 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -1,17 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using System.Net; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// HTTP message handler which resolves endpoints using service discovery. /// -public class ResolvingHttpDelegatingHandler : DelegatingHandler +internal sealed class ResolvingHttpDelegatingHandler : DelegatingHandler { private readonly HttpServiceEndPointResolver _resolver; private readonly ServiceDiscoveryOptions _options; @@ -43,29 +41,19 @@ public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOpt protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var originalUri = request.RequestUri; - IEndPointHealthFeature? epHealth = null; - Exception? error = null; - var startTime = Stopwatch.GetTimestamp(); if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); request.RequestUri = GetUriWithEndPoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; - epHealth = result.Features.Get(); } try { return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } - catch (Exception exception) - { - error = exception; - throw; - } finally { - epHealth?.ReportHealth(Stopwatch.GetElapsedTime(startTime), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. request.RequestUri = originalUri; } } @@ -124,7 +112,7 @@ internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, if (uri.Scheme.IndexOf('+') > 0) { var scheme = uri.Scheme.Split('+')[0]; - if (options.AllowedSchemes.Equals(ServiceDiscoveryOptions.AllSchemes) || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) + if (options.AllowedSchemes.Equals(ServiceDiscoveryOptions.AllowAllSchemes) || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) { result.Scheme = scheme; } diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs new file mode 100644 index 0000000000..0d3ba00122 --- /dev/null +++ b/src/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Http; + +internal sealed class ServiceDiscoveryHttpMessageHandlerFactory( + TimeProvider timeProvider, + IServiceProvider serviceProvider, + ServiceEndPointWatcherFactory factory, + IOptions options) : IServiceDiscoveryHttpMessageHandlerFactory +{ + public HttpMessageHandler CreateHandler(HttpMessageHandler handler) + { + var registry = new HttpServiceEndPointResolver(factory, serviceProvider, timeProvider); + return new ResolvingHttpDelegatingHandler(registry, options, handler); + } +} diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs b/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs new file mode 100644 index 0000000000..07bffa5654 --- /dev/null +++ b/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.ServiceDiscovery.Internal; + +/// +/// Represents the result of service endpoint resolution. +/// +/// The endpoint collection. +/// The exception which occurred during resolution. +internal sealed class ServiceEndPointResolverResult(ServiceEndPointSource? endPointSource, Exception? exception) +{ + /// + /// Gets the exception which occurred during resolution. + /// + public Exception? Exception { get; } = exception; + + /// + /// Gets a value indicating whether resolution completed successfully. + /// + [MemberNotNullWhen(true, nameof(EndPointSource))] + public bool ResolvedSuccessfully => Exception is null; + + /// + /// Gets the endpoints. + /// + public ServiceEndPointSource? EndPointSource { get; } = endPointSource; +} diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs b/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs deleted file mode 100644 index de04748187..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.ServiceDiscovery.Internal; - -internal sealed class ServiceNameParser(IOptions options) -{ - private readonly string[] _allowedSchemes = options.Value.AllowedSchemes; - - public bool TryParse(string serviceName, [NotNullWhen(true)] out ServiceNameParts parts) - { - if (serviceName.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) - { - parts = Create(uri, hasScheme: false); - return true; - } - - if (Uri.TryCreate(serviceName, default, out uri)) - { - parts = Create(uri, hasScheme: true); - return true; - } - - parts = default; - return false; - - ServiceNameParts Create(Uri uri, bool hasScheme) - { - var uriHost = uri.Host; - var segmentSeparatorIndex = uriHost.IndexOf('.'); - string host; - string? endPointName = null; - var port = uri.Port > 0 ? uri.Port : 0; - if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') - { - endPointName = uriHost[1..segmentSeparatorIndex]; - - // Skip the endpoint name, including its prefix ('_') and suffix ('.'). - host = uriHost[(segmentSeparatorIndex + 1)..]; - } - else - { - host = uriHost; - } - - // Allow multiple schemes to be separated by a '+', eg. "https+http://host:port". - var schemes = hasScheme ? ParseSchemes(uri.Scheme) : []; - return new(schemes, host, endPointName, port); - } - } - - private string[] ParseSchemes(string scheme) - { - if (_allowedSchemes.Equals(ServiceDiscoveryOptions.AllSchemes)) - { - return scheme.Split('+'); - } - - List result = []; - foreach (var s in scheme.Split('+')) - { - foreach (var allowed in _allowedSchemes) - { - if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) - { - result.Add(s); - break; - } - } - } - - return result.ToArray(); - } -} - diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs deleted file mode 100644 index f93729a40e..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Internal; - -internal readonly struct ServiceNameParts : IEquatable -{ - public ServiceNameParts(string[] schemePriority, string host, string? endPointName, int port) : this() - { - Schemes = schemePriority; - Host = host; - EndPointName = endPointName; - Port = port; - } - - public string? EndPointName { get; init; } - - public string[] Schemes { get; init; } - - public string Host { get; init; } - - public int Port { get; init; } - - public override string? ToString() => EndPointName is not null ? $"EndPointName: {EndPointName}, Host: {Host}, Port: {Port}" : $"Host: {Host}, Port: {Port}"; - - public override bool Equals(object? obj) => obj is ServiceNameParts other && Equals(other); - - public override int GetHashCode() => HashCode.Combine(EndPointName, Host, Port); - - public bool Equals(ServiceNameParts other) => - EndPointName == other.EndPointName && - Host == other.Host && - Port == other.Port; - - public static bool operator ==(ServiceNameParts left, ServiceNameParts right) => left.Equals(right); - - public static bool operator !=(ServiceNameParts left, ServiceNameParts right) => !(left == right); -} - diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs similarity index 73% rename from src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs rename to src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs index e2ffbd0421..bd0172c45c 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs @@ -1,21 +1,21 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing; /// /// Selects endpoints from a collection of endpoints. /// -public interface IServiceEndPointSelector +internal interface IServiceEndPointSelector { /// /// Sets the collection of endpoints which this instance will select from. /// /// The collection of endpoints to select from. - void SetEndPoints(ServiceEndPointCollection endPoints); + void SetEndPoints(ServiceEndPointSource endPoints); /// - /// Selects an endpoints from the collection provided by the most recent call to . + /// Selects an endpoints from the collection provided by the most recent call to . /// /// The context. /// An endpoint. diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs deleted file mode 100644 index 9395896e52..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// A service endpoint selector which always returns the first endpoint in a collection. -/// -public class PickFirstServiceEndPointSelector : IServiceEndPointSelector -{ - private ServiceEndPointCollection? _endPoints; - - /// - public ServiceEndPoint GetEndPoint(object? context) - { - if (_endPoints is not { Count: > 0 } endPoints) - { - throw new InvalidOperationException("The endpoint collection contains no endpoints"); - } - - return endPoints[0]; - } - - /// - public void SetEndPoints(ServiceEndPointCollection endPoints) - { - _endPoints = endPoints; - } -} diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs deleted file mode 100644 index d3f657c955..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Provides instances of . -/// -public class PickFirstServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider -{ - /// - /// Gets a shared instance of this class. - /// - public static PickFirstServiceEndPointSelectorProvider Instance { get; } = new(); - - /// - public IServiceEndPointSelector CreateSelector() => new PickFirstServiceEndPointSelector(); -} diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs deleted file mode 100644 index e233dfb7b5..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Selects endpoints using the Power of Two Choices algorithm for distributed load balancing based on -/// the last-known load of the candidate endpoints. -/// -public class PowerOfTwoChoicesServiceEndPointSelector : IServiceEndPointSelector -{ - private ServiceEndPointCollection? _endPoints; - - /// - public void SetEndPoints(ServiceEndPointCollection endPoints) - { - _endPoints = endPoints; - } - - /// - public ServiceEndPoint GetEndPoint(object? context) - { - if (_endPoints is not { Count: > 0 } collection) - { - throw new InvalidOperationException("The endpoint collection contains no endpoints"); - } - - if (collection.Count == 1) - { - return collection[0]; - } - - var first = collection[Random.Shared.Next(collection.Count)]; - ServiceEndPoint second; - do - { - second = collection[Random.Shared.Next(collection.Count)]; - } while (ReferenceEquals(first, second)); - - // Note that this relies on fresh data to be effective. - if (first.Features.Get() is { } firstLoad - && second.Features.Get() is { } secondLoad) - { - return firstLoad.CurrentLoad < secondLoad.CurrentLoad ? first : second; - } - - // Degrade to random. - return first; - } -} diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs deleted file mode 100644 index 00832bc781..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Provides instances of . -/// -public class PowerOfTwoChoicesServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider -{ - /// - /// Gets a shared instance of this class. - /// - public static PowerOfTwoChoicesServiceEndPointSelectorProvider Instance { get; } = new(); - - /// - public IServiceEndPointSelector CreateSelector() => new PowerOfTwoChoicesServiceEndPointSelector(); -} diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs deleted file mode 100644 index 8e4bb2378d..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// A service endpoint selector which returns random endpoints from the collection. -/// -public class RandomServiceEndPointSelector : IServiceEndPointSelector -{ - private ServiceEndPointCollection? _endPoints; - - /// - public void SetEndPoints(ServiceEndPointCollection endPoints) - { - _endPoints = endPoints; - } - - /// - public ServiceEndPoint GetEndPoint(object? context) - { - if (_endPoints is not { Count: > 0 } collection) - { - throw new InvalidOperationException("The endpoint collection contains no endpoints"); - } - - return collection[Random.Shared.Next(collection.Count)]; - } -} diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs deleted file mode 100644 index ae74b4032b..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Provides instances of . -/// -public class RandomServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider -{ - /// - /// Gets a shared instance of this class. - /// - public static RandomServiceEndPointSelectorProvider Instance { get; } = new(); - - /// - public IServiceEndPointSelector CreateSelector() => new RandomServiceEndPointSelector(); -} diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs index 5848c7d8f7..92da7cf25b 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs @@ -1,20 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing; /// /// Selects endpoints by iterating through the list of endpoints in a round-robin fashion. /// -public class RoundRobinServiceEndPointSelector : IServiceEndPointSelector +internal sealed class RoundRobinServiceEndPointSelector : IServiceEndPointSelector { private uint _next; - private ServiceEndPointCollection? _endPoints; + private IReadOnlyList? _endPoints; /// - public void SetEndPoints(ServiceEndPointCollection endPoints) + public void SetEndPoints(ServiceEndPointSource endPoints) { - _endPoints = endPoints; + _endPoints = endPoints.EndPoints; } /// diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs deleted file mode 100644 index 40d9ce7845..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Provides instances of . -/// -public class RoundRobinServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider -{ - /// - /// Gets a shared instance of this class. - /// - public static RoundRobinServiceEndPointSelectorProvider Instance { get; } = new(); - - /// - public IServiceEndPointSelector CreateSelector() => new RoundRobinServiceEndPointSelector(); -} diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 6836e58cf6..9a5d67db04 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -10,7 +10,8 @@ - + + diff --git a/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs index ab0ea286b6..483c08702d 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs @@ -3,7 +3,6 @@ using System.Net; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; @@ -12,18 +11,17 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointProvider { - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) + public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) { - if (endPoints.EndPoints.Count != 0) + if (endPoints.EndPoints.Count == 0) { - return new(ResolutionStatus.None); + Log.UsingPassThrough(logger, serviceName); + var ep = ServiceEndPoint.Create(endPoint); + ep.Features.Set(this); + endPoints.EndPoints.Add(ep); } - Log.UsingPassThrough(logger, serviceName); - var ep = ServiceEndPoint.Create(endPoint); - ep.Features.Set(this); - endPoints.EndPoints.Add(ep); - return new(ResolutionStatus.Success); + return default; } public ValueTask DisposeAsync() => default; diff --git a/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs index b3a326010b..83455e0979 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs @@ -4,18 +4,18 @@ using System.Diagnostics.CodeAnalysis; using System.Net; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// /// Service endpoint resolver provider which passes through the provided value. /// -internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointResolverProvider +internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointProviderFactory { /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { + var serviceName = query.OriginalString; if (!TryCreateEndPoint(serviceName, out var endPoint)) { // Propagate the value through regardless, leaving it to the caller to interpret it. diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs similarity index 73% rename from src/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs rename to src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index c1c833de89..bcfa59056c 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs @@ -2,11 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Http; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.Http; namespace Microsoft.Extensions.DependencyInjection; @@ -14,49 +12,30 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// Extensions for configuring with service discovery. /// -public static class HttpClientBuilderExtensions +public static class ServiceDiscoveryHttpClientBuilderExtensions { /// /// Adds service discovery to the . /// /// The builder. - /// The provider that creates selector instances. /// The builder. - public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder, IServiceEndPointSelectorProvider selectorProvider) - { - var services = httpClientBuilder.Services; - services.AddServiceDiscoveryCore(); - httpClientBuilder.AddHttpMessageHandler(services => - { - var timeProvider = services.GetService() ?? TimeProvider.System; - var resolverProvider = services.GetRequiredService(); - var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); - var options = services.GetRequiredService>(); - return new ResolvingHttpDelegatingHandler(registry, options); - }); - - // Configure the HttpClient to disable gRPC load balancing. - // This is done on all HttpClient instances but only impacts gRPC clients. - AddDisableGrpcLoadBalancingFilter(httpClientBuilder.Services, httpClientBuilder.Name); - - return httpClientBuilder; - } + [Obsolete(error: true, message: "This method is obsolete and will be removed in a future version. The recommended alternative is to use the 'AddServiceDiscovery' method instead.")] + public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder) => httpClientBuilder.AddServiceDiscovery(); /// /// Adds service discovery to the . /// /// The builder. /// The builder. - public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder) + public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder httpClientBuilder) { var services = httpClientBuilder.Services; services.AddServiceDiscoveryCore(); httpClientBuilder.AddHttpMessageHandler(services => { var timeProvider = services.GetService() ?? TimeProvider.System; - var selectorProvider = services.GetRequiredService(); - var resolverProvider = services.GetRequiredService(); - var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); + var resolverProvider = services.GetRequiredService(); + var registry = new HttpServiceEndPointResolver(resolverProvider, services, timeProvider); var options = services.GetRequiredService>(); return new ResolvingHttpDelegatingHandler(registry, options); }); diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs index d9510a3cf2..89c5a2d2eb 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs @@ -1,29 +1,63 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Primitives; + namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Options for configuring service discovery. +/// Options for service endpoint resolvers. /// public sealed class ServiceDiscoveryOptions { /// - /// The value for which indicates that all schemes are allowed. + /// The value indicating that all endpoint schemes are allowed. /// #pragma warning disable IDE0300 // Simplify collection initialization #pragma warning disable CA1825 // Avoid zero-length array allocations - public static readonly string[] AllSchemes = new string[0]; + public static readonly string[] AllowAllSchemes = new string[0]; #pragma warning restore CA1825 // Avoid zero-length array allocations #pragma warning restore IDE0300 // Simplify collection initialization + /// + /// Gets or sets the period between polling attempts for resolvers which do not support refresh notifications via . + /// + public TimeSpan RefreshPeriod { get; set; } = TimeSpan.FromSeconds(60); + /// /// Gets or sets the collection of allowed URI schemes for URIs resolved by the service discovery system when multiple schemes are specified, for example "https+http://_endpoint.service". /// /// - /// When set to , all schemes are allowed. + /// When set to , all schemes are allowed. /// Schemes are not case-sensitive. /// - public string[] AllowedSchemes { get; set; } = AllSchemes; -} + public string[] AllowedSchemes { get; set; } = AllowAllSchemes; + internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IReadOnlyList allowedSchemes) + { + if (allowedSchemes.Equals(AllowAllSchemes)) + { + if (schemes is string[] array) + { + return array; + } + + return schemes.ToArray(); + } + + List result = []; + foreach (var s in schemes) + { + foreach (var allowed in allowedSchemes) + { + if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) + { + result.Add(s); + break; + } + } + } + + return result.ToArray(); + } +} diff --git a/src/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs similarity index 57% rename from src/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs rename to src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs index a4ba9b63b3..6403b21463 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs @@ -2,20 +2,21 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Configuration; +using Microsoft.Extensions.ServiceDiscovery.Http; using Microsoft.Extensions.ServiceDiscovery.Internal; +using Microsoft.Extensions.ServiceDiscovery.LoadBalancing; using Microsoft.Extensions.ServiceDiscovery.PassThrough; -namespace Microsoft.Extensions.Hosting; +namespace Microsoft.Extensions.DependencyInjection; /// /// Extension methods for configuring service discovery. /// -public static class HostingExtensions +public static class ServiceDiscoveryServiceCollectionExtensions { /// /// Adds the core service discovery services and configures defaults. @@ -29,21 +30,47 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser .AddPassThroughServiceEndPointResolver(); } + /// + /// Adds the core service discovery services and configures defaults. + /// + /// The service collection. + /// The delegate used to configure service discovery options. + /// The service collection. + public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, Action? configureOptions) + { + return services.AddServiceDiscoveryCore(configureOptions: configureOptions) + .AddConfigurationServiceEndPointResolver() + .AddPassThroughServiceEndPointResolver(); + } + /// /// Adds the core service discovery services. /// /// The service collection. /// The service collection. - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => services.AddServiceDiscoveryCore(configureOptions: null); + + /// + /// Adds the core service discovery services. + /// + /// The service collection. + /// The delegate used to configure service discovery options. + /// The service collection. + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action? configureOptions) { services.AddOptions(); services.AddLogging(); - services.TryAddSingleton(); services.TryAddTransient, ServiceDiscoveryOptionsValidator>(); - services.TryAddSingleton(static sp => TimeProvider.System); - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(sp => new ServiceEndPointResolver(sp.GetRequiredService(), sp.GetRequiredService())); + services.TryAddSingleton(_ => TimeProvider.System); + services.TryAddTransient(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(sp => new ServiceEndPointResolver(sp.GetRequiredService(), sp.GetRequiredService())); + if (configureOptions is not null) + { + services.Configure(configureOptions); + } + return services; } @@ -63,10 +90,10 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS /// The delegate used to configure the provider. /// The service collection. /// The service collection. - public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) + public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action? configureOptions) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); + services.AddSingleton(); services.AddTransient, ConfigurationServiceEndPointResolverOptionsValidator>(); if (configureOptions is not null) { @@ -84,7 +111,7 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS public static IServiceCollection AddPassThroughServiceEndPointResolver(this IServiceCollection services) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); + services.AddSingleton(); return services; } } diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs new file mode 100644 index 0000000000..1a14cb961b --- /dev/null +++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// A mutable collection of service endpoints. +/// +internal sealed class ServiceEndPointBuilder : IServiceEndPointBuilder +{ + private readonly List _endPoints = new(); + private readonly List _changeTokens = new(); + private readonly FeatureCollection _features = new FeatureCollection(); + + /// + /// Adds a change token. + /// + /// The change token. + public void AddChangeToken(IChangeToken changeToken) + { + _changeTokens.Add(changeToken); + } + + /// + /// Gets the feature collection. + /// + public IFeatureCollection Features => _features; + + /// + /// Gets the endpoints. + /// + public IList EndPoints => _endPoints; + + /// + /// Creates a from the provided instance. + /// + /// The service endpoint source. + public ServiceEndPointSource Build() + { + return new ServiceEndPointSource(_endPoints, new CompositeChangeToken(_changeTokens), _features); + } +} + diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index 9de4a61b41..029d260124 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using System.Diagnostics; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery; @@ -16,7 +15,7 @@ public sealed class ServiceEndPointResolver : IAsyncDisposable private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); private readonly object _lock = new(); - private readonly ServiceEndPointResolverFactory _resolverProvider; + private readonly ServiceEndPointWatcherFactory _resolverProvider; private readonly TimeProvider _timeProvider; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; @@ -28,7 +27,7 @@ public sealed class ServiceEndPointResolver : IAsyncDisposable /// /// The resolver factory. /// The time provider. - internal ServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider, TimeProvider timeProvider) + internal ServiceEndPointResolver(ServiceEndPointWatcherFactory resolverProvider, TimeProvider timeProvider) { _resolverProvider = resolverProvider; _timeProvider = timeProvider; @@ -40,7 +39,7 @@ internal ServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider /// The service name. /// A . /// The resolved service endpoints. - public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) + public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(serviceName); ObjectDisposedException.ThrowIf(_disposed, this); @@ -157,7 +156,7 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverProvider.CreateResolver(serviceName); + var resolver = _resolverProvider.CreateWatcher(serviceName); resolver.Start(); return new ResolverEntry(resolver); } @@ -182,7 +181,7 @@ public bool CanExpire() return (status & (CountMask | RecentUseFlag)) == 0; } - public async ValueTask<(bool Valid, ServiceEndPointCollection? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) + public async ValueTask<(bool Valid, ServiceEndPointSource? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) { try { diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs deleted file mode 100644 index 415a2192c3..0000000000 --- a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.Primitives; - -namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; - -/// -/// Options for service endpoint resolvers. -/// -public sealed class ServiceEndPointResolverOptions -{ - /// - /// Gets or sets the period between polling resolvers which are in a pending state and do not support refresh notifications via . - /// - public TimeSpan PendingStatusRefreshPeriod { get; set; } = TimeSpan.FromSeconds(15); - - /// - /// Gets or sets the period between polling attempts for resolvers which do not support refresh notifications via . - /// - public TimeSpan RefreshPeriod { get; set; } = TimeSpan.FromSeconds(60); -} diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs index 1786481106..78a8f84b55 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery; @@ -19,11 +18,11 @@ private sealed partial class Log [LoggerMessage(3, LogLevel.Debug, "Resolved {Count} endpoints for service '{ServiceName}': {EndPoints}.", EventName = "ResolutionSucceeded")] public static partial void ResolutionSucceededCore(ILogger logger, int count, string serviceName, string endPoints); - public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndPointCollection endPoints) + public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndPointSource endPointSource) { if (logger.IsEnabled(LogLevel.Debug)) { - ResolutionSucceededCore(logger, endPoints.Count, serviceName, string.Join(", ", endPoints.Select(GetEndPointString))); + ResolutionSucceededCore(logger, endPointSource.EndPoints.Count, serviceName, string.Join(", ", endPointSource.EndPoints.Select(GetEndPointString))); } static string GetEndPointString(ServiceEndPoint ep) diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs index 8936d3722b..9b1069d31e 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs @@ -4,34 +4,32 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.ExceptionServices; -using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery; /// /// Watches for updates to the collection of resolved endpoints for a specified service. /// -public sealed partial class ServiceEndPointWatcher( +internal sealed partial class ServiceEndPointWatcher( IServiceEndPointProvider[] resolvers, ILogger logger, string serviceName, TimeProvider timeProvider, - IOptions options) : IAsyncDisposable + IOptions options) : IAsyncDisposable { private static readonly TimerCallback s_pollingAction = static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: true); private readonly object _lock = new(); private readonly ILogger _logger = logger; private readonly TimeProvider _timeProvider = timeProvider; - private readonly ServiceEndPointResolverOptions _options = options.Value; + private readonly ServiceDiscoveryOptions _options = options.Value; private readonly IServiceEndPointProvider[] _resolvers = resolvers; private readonly CancellationTokenSource _disposalCancellation = new(); private ITimer? _pollingTimer; - private ServiceEndPointCollection? _cachedEndPoints; + private ServiceEndPointSource? _cachedEndPoints; private Task _refreshTask = Task.CompletedTask; private volatile CacheStatus _cacheState; @@ -59,23 +57,23 @@ public void Start() /// /// A . /// A collection of resolved endpoints for the service. - public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) + public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) { ThrowIfNoResolvers(); // If the cache is valid, return the cached value. if (_cachedEndPoints is { ChangeToken.HasChanged: false } cached) { - return new ValueTask(cached); + return new ValueTask(cached); } // Otherwise, ensure the cache is being refreshed // Wait for the cache refresh to complete and return the cached value. return GetEndPointsInternal(cancellationToken); - async ValueTask GetEndPointsInternal(CancellationToken cancellationToken) + async ValueTask GetEndPointsInternal(CancellationToken cancellationToken) { - ServiceEndPointCollection? result; + ServiceEndPointSource? result; do { await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); @@ -126,79 +124,48 @@ private async Task RefreshAsyncInternal() await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); var cancellationToken = _disposalCancellation.Token; Exception? error = null; - ServiceEndPointCollection? newEndPoints = null; + ServiceEndPointSource? newEndPoints = null; CacheStatus newCacheState; - ResolutionStatus status = ResolutionStatus.Success; - while (true) + try { - try + Log.ResolvingEndPoints(_logger, ServiceName); + var builder = new ServiceEndPointBuilder(); + foreach (var resolver in _resolvers) { - var collection = new ServiceEndPointCollectionSource(ServiceName, new FeatureCollection()); - status = ResolutionStatus.Success; - Log.ResolvingEndPoints(_logger, ServiceName); - foreach (var resolver in _resolvers) - { - var resolverStatus = await resolver.ResolveAsync(collection, cancellationToken).ConfigureAwait(false); - status = CombineStatus(status, resolverStatus); - } + await resolver.PopulateAsync(builder, cancellationToken).ConfigureAwait(false); + } - var endPoints = ServiceEndPointCollectionSource.CreateServiceEndPointCollection(collection); - var statusCode = status.StatusCode; - if (statusCode != ResolutionStatusCode.Success) + var endPoints = builder.Build(); + newCacheState = CacheStatus.Valid; + + lock (_lock) + { + // Check if we need to poll for updates or if we can register for change notification callbacks. + if (endPoints.ChangeToken.ActiveChangeCallbacks) { - if (statusCode is ResolutionStatusCode.Pending) - { - // Wait until a timeout or the collection's ChangeToken.HasChange becomes true and try again. - Log.ResolutionPending(_logger, ServiceName); - await WaitForPendingChangeToken(endPoints.ChangeToken, _options.PendingStatusRefreshPeriod, cancellationToken).ConfigureAwait(false); - continue; - } - else if (statusCode is ResolutionStatusCode.Cancelled) + // Initiate a background refresh, if necessary. + endPoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: false), this); + if (_pollingTimer is { } timer) { - newCacheState = CacheStatus.Invalid; - error = status.Exception ?? new OperationCanceledException(); - break; - } - else if (statusCode is ResolutionStatusCode.Error) - { - newCacheState = CacheStatus.Invalid; - error = status.Exception; - break; + _pollingTimer = null; + timer.Dispose(); } } - - lock (_lock) + else { - // Check if we need to poll for updates or if we can register for change notification callbacks. - if (endPoints.ChangeToken.ActiveChangeCallbacks) - { - // Initiate a background refresh, if necessary. - endPoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: false), this); - if (_pollingTimer is { } timer) - { - _pollingTimer = null; - timer.Dispose(); - } - } - else - { - SchedulePollingTimer(); - } - - // The cache is valid - newEndPoints = endPoints; - newCacheState = CacheStatus.Valid; - break; + SchedulePollingTimer(); } + + // The cache is valid + newEndPoints = endPoints; + newCacheState = CacheStatus.Valid; } - catch (Exception exception) - { - error = exception; - newCacheState = CacheStatus.Invalid; - SchedulePollingTimer(); - status = CombineStatus(status, ResolutionStatus.FromException(exception)); - break; - } + } + catch (Exception exception) + { + error = exception; + newCacheState = CacheStatus.Invalid; + SchedulePollingTimer(); } // If there was an error, the cache must be invalid. @@ -215,7 +182,7 @@ private async Task RefreshAsyncInternal() if (OnEndPointsUpdated is { } callback) { - callback(new(newEndPoints, status)); + callback(new(newEndPoints, error)); } lock (_lock) @@ -255,48 +222,6 @@ private void SchedulePollingTimer() } } - private static ResolutionStatus CombineStatus(ResolutionStatus existing, ResolutionStatus newStatus) - { - if (existing.StatusCode > newStatus.StatusCode) - { - return existing; - } - - var code = (ResolutionStatusCode)Math.Max((int)existing.StatusCode, (int)newStatus.StatusCode); - Exception? exception; - if (existing.Exception is not null && newStatus.Exception is not null) - { - List exceptions = new(); - AddExceptions(existing.Exception, exceptions); - AddExceptions(newStatus.Exception, exceptions); - exception = new AggregateException(exceptions); - } - else - { - exception = existing.Exception ?? newStatus.Exception; - } - - var message = code switch - { - ResolutionStatusCode.Error => exception!.Message ?? "Error", - _ => code.ToString(), - }; - - return new ResolutionStatus(code, exception, message); - - static void AddExceptions(Exception? exception, List exceptions) - { - if (exception is AggregateException ae) - { - exceptions.AddRange(ae.InnerExceptions); - } - else if (exception is not null) - { - exceptions.Add(exception); - } - } - } - /// public async ValueTask DisposeAsync() { @@ -328,48 +253,6 @@ private enum CacheStatus Valid } - private static async Task WaitForPendingChangeToken(IChangeToken changeToken, TimeSpan pollPeriod, CancellationToken cancellationToken) - { - if (changeToken.HasChanged) - { - return; - } - - TaskCompletionSource completion = new(TaskCreationOptions.RunContinuationsAsynchronously); - IDisposable? changeTokenRegistration = null; - IDisposable? cancellationRegistration = null; - IDisposable? pollPeriodRegistration = null; - CancellationTokenSource? timerCancellation = null; - - try - { - // Either wait for a callback or poll externally. - if (changeToken.ActiveChangeCallbacks) - { - changeTokenRegistration = changeToken.RegisterChangeCallback(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); - } - else - { - timerCancellation = new(pollPeriod); - pollPeriodRegistration = timerCancellation.Token.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); - } - - if (cancellationToken.CanBeCanceled) - { - cancellationRegistration = cancellationToken.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); - } - - await completion.Task.ConfigureAwait(false); - } - finally - { - changeTokenRegistration?.Dispose(); - cancellationRegistration?.Dispose(); - pollPeriodRegistration?.Dispose(); - timerCancellation?.Dispose(); - } - } - private void ThrowIfNoResolvers() { if (_resolvers.Length == 0) diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs similarity index 90% rename from src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs rename to src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs index d7835f26d0..69f565eb8e 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs @@ -2,11 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery; -partial class ServiceEndPointResolverFactory +partial class ServiceEndPointWatcherFactory { private sealed partial class Log { diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs similarity index 69% rename from src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs rename to src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs index c545c82e9e..90f62ab059 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs @@ -3,38 +3,42 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.PassThrough; namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Creates service endpoint resolvers. +/// Creates service endpoint watchers. /// -public partial class ServiceEndPointResolverFactory( - IEnumerable resolvers, +internal sealed partial class ServiceEndPointWatcherFactory( + IEnumerable resolvers, ILogger resolverLogger, - IOptions options, + IOptions options, TimeProvider timeProvider) { - private readonly IServiceEndPointResolverProvider[] _resolverProviders = resolvers + private readonly IServiceEndPointProviderFactory[] _resolverProviders = resolvers .Where(r => r is not PassThroughServiceEndPointResolverProvider) .Concat(resolvers.Where(static r => r is PassThroughServiceEndPointResolverProvider)).ToArray(); private readonly ILogger _logger = resolverLogger; private readonly TimeProvider _timeProvider = timeProvider; - private readonly IOptions _options = options; + private readonly IOptions _options = options; /// /// Creates a service endpoint resolver for the provided service name. /// - public ServiceEndPointWatcher CreateResolver(string serviceName) + public ServiceEndPointWatcher CreateWatcher(string serviceName) { ArgumentNullException.ThrowIfNull(serviceName); + if (!ServiceEndPointQuery.TryParse(serviceName, out var query)) + { + throw new ArgumentException("The provided input was not in a valid format. It must be a valid URI.", nameof(serviceName)); + } + List? resolvers = null; foreach (var factory in _resolverProviders) { - if (factory.TryCreateResolver(serviceName, out var resolver)) + if (factory.TryCreateProvider(query, out var resolver)) { resolvers ??= []; resolvers.Add(resolver); diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs b/src/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs similarity index 86% rename from src/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs rename to src/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs index 6d3132da88..6b5b07d199 100644 --- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs +++ b/src/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net; @@ -9,7 +9,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// An endpoint represented by a . /// /// The . -public sealed class UriEndPoint(Uri uri) : EndPoint +internal sealed class UriEndPoint(Uri uri) : EndPoint { /// /// Gets the associated with this endpoint. diff --git a/tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index 7e2ef478ef..25cd88a143 100644 --- a/tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs @@ -8,14 +8,14 @@ using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; using Xunit; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; /// /// Tests for and . -/// These also cover and by extension. +/// These also cover and by extension. /// public class DnsSrvServiceEndPointResolverTests { @@ -103,9 +103,9 @@ public async Task ResolveServiceEndPoint_Dns() .AddServiceDiscoveryCore() .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -114,14 +114,13 @@ public async Task ResolveServiceEndPoint_Dns() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(3, initialResult.EndPoints.Count); - var eps = initialResult.EndPoints; + Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); + var eps = initialResult.EndPointSource.EndPoints; Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -190,9 +189,9 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns"); }; var services = serviceCollection.BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -200,20 +199,19 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo resolver.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); - Assert.Null(initialResult.Status.Exception); + Assert.Null(initialResult.Exception); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); if (dnsFirst) { // We expect only the results from the DNS provider. - Assert.Equal(3, initialResult.EndPoints.Count); - var eps = initialResult.EndPoints; + Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); + var eps = initialResult.EndPointSource.EndPoints; Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint); Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -223,11 +221,11 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo else { // We expect only the results from the Configuration provider. - Assert.Equal(2, initialResult.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPoints[1].EndPoint); + Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count); + Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPointSource.EndPoints[1].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -271,9 +269,9 @@ public async Task ResolveServiceEndPoint_Dns_RespectsChangeToken() .AddServiceDiscovery() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointResolver resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var channel = Channel.CreateUnbounded(); diff --git a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs index f35ffa2026..6d8091f026 100644 --- a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs @@ -5,15 +5,15 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Configuration; +using Microsoft.Extensions.ServiceDiscovery.Internal; using Xunit; namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for and . -/// These also cover and by extension. +/// Tests for . +/// These also cover and by extension. /// public class ConfigurationServiceEndPointResolverTests { @@ -29,9 +29,9 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() .AddServiceDiscoveryCore() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -40,11 +40,10 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - var ep = Assert.Single(initialResult.EndPoints); + var ep = Assert.Single(initialResult.EndPointSource.EndPoints); Assert.Equal(new DnsEndPoint("localhost", 8080), ep.EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -67,12 +66,12 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() .AddConfigurationServiceEndPointResolver() .Configure(o => o.AllowedSchemes = ["https"]) .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; // Explicitly specifying http. // We should get no endpoint back because http is not allowed by configuration. - await using ((resolver = resolverFactory.CreateResolver("http://_foo.basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -81,13 +80,12 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Empty(initialResult.EndPoints); + Assert.Empty(initialResult.EndPointSource.EndPoints); } // Specifying either https or http. // The result should be that we only get the http endpoint back. - await using ((resolver = resolverFactory.CreateResolver("https+http://_foo.basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -96,14 +94,13 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - var ep = Assert.Single(initialResult.EndPoints); + var ep = Assert.Single(initialResult.EndPointSource.EndPoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); } // Specifying either https or http, but in reverse. // The result should be that we only get the http endpoint back. - await using ((resolver = resolverFactory.CreateResolver("http+https://_foo.basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -112,8 +109,7 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - var ep = Assert.Single(initialResult.EndPoints); + var ep = Assert.Single(initialResult.EndPointSource.EndPoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); } } @@ -135,9 +131,9 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() .AddServiceDiscoveryCore() .AddConfigurationServiceEndPointResolver(options => options.ApplyHostNameMetadata = _ => true) .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -146,12 +142,11 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(2, initialResult.EndPoints.Count); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPoints[1].EndPoint); + Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPointSource.EndPoints[1].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -160,7 +155,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() } // Request either https or http. Since there are only http endpoints, we should get only http endpoints back. - await using ((resolver = resolverFactory.CreateResolver("https+http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("https+http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -169,12 +164,11 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(2, initialResult.EndPoints.Count); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPoints[1].EndPoint); + Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPointSource.EndPoints[1].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -204,9 +198,9 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() .AddServiceDiscoveryCore() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://_grpc.basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://_grpc.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -215,13 +209,12 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(3, initialResult.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPoints[1].EndPoint); - Assert.Equal(new UriEndPoint(new Uri("http://remotehost:4444")), initialResult.EndPoints[2].EndPoint); + Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); + Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPointSource.EndPoints[1].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://remotehost:4444")), initialResult.EndPointSource.EndPoints[2].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -250,9 +243,9 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpe .AddServiceDiscoveryCore() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("https+http://_grpc.basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("https+http://_grpc.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -261,17 +254,16 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpe var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(3, initialResult.EndPoints.Count); + Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); // These must be treated as HTTPS by the HttpClient middleware, but that is not the responsibility of the resolver. - Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPoints[1].EndPoint); + Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPointSource.EndPoints[1].EndPoint); // We expect the HTTPS endpoint back but not the HTTP one. - Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndPoints[2].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndPointSource.EndPoints[2].EndPoint); - Assert.All(initialResult.EndPoints, ep => + Assert.All(initialResult.EndPointSource.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); diff --git a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index d8adcbca52..643bbfad44 100644 --- a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -5,8 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; using Microsoft.Extensions.ServiceDiscovery.PassThrough; using Xunit; @@ -14,7 +13,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// /// Tests for . -/// These also cover and by extension. +/// These also cover and by extension. /// public class PassThroughServiceEndPointResolverTests { @@ -25,9 +24,9 @@ public async Task ResolveServiceEndPoint_PassThrough() .AddServiceDiscoveryCore() .AddPassThroughServiceEndPointResolver() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -36,8 +35,7 @@ public async Task ResolveServiceEndPoint_PassThrough() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); - var ep = Assert.Single(initialResult.EndPoints); + var ep = Assert.Single(initialResult.EndPointSource.EndPoints); Assert.Equal(new DnsEndPoint("basket", 80), ep.EndPoint); } } @@ -57,9 +55,9 @@ public async Task ResolveServiceEndPoint_Superseded() .AddSingleton(config.Build()) .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -68,11 +66,10 @@ public async Task ResolveServiceEndPoint_Superseded() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); // We expect the basket service to be resolved from Configuration, not the pass-through provider. - Assert.Single(initialResult.EndPoints); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); + Assert.Single(initialResult.EndPointSource.EndPoints); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); } } @@ -91,9 +88,9 @@ public async Task ResolveServiceEndPoint_Fallback() .AddSingleton(config.Build()) .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://catalog")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://catalog")).ConfigureAwait(false)) { Assert.NotNull(resolver); var tcs = new TaskCompletionSource(); @@ -102,11 +99,10 @@ public async Task ResolveServiceEndPoint_Fallback() var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatus.Success, initialResult.Status); // We expect the CATALOG service to be resolved from the pass-through provider. - Assert.Single(initialResult.EndPoints); - Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndPoints[0].EndPoint); + Assert.Single(initialResult.EndPointSource.EndPoints); + Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndPointSource.EndPoints[0].EndPoint); } } @@ -128,7 +124,7 @@ public async Task ResolveServiceEndPoint_Fallback_NoScheme() .BuildServiceProvider(); var resolver = services.GetRequiredService(); - var endPoints = await resolver.GetEndPointsAsync("catalog", default); - Assert.Equal(new DnsEndPoint("catalog", 0), endPoints[0].EndPoint); + var result = await resolver.GetEndPointsAsync("catalog", default); + Assert.Equal(new DnsEndPoint("catalog", 0), result.EndPoints[0].EndPoint); } } diff --git a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index 0628bedbe7..f5e506a9b7 100644 --- a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -8,14 +8,14 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Primitives; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.Http; +using Microsoft.Extensions.ServiceDiscovery.Internal; using Xunit; namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for and . +/// Tests for and . /// public class ServiceEndPointResolverTests { @@ -25,8 +25,8 @@ public void ResolveServiceEndPoint_NoResolversConfigured_Throws() var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - var exception = Assert.Throws(() => resolverFactory.CreateResolver("https://basket")); + var resolverFactory = services.GetRequiredService(); + var exception = Assert.Throws(() => resolverFactory.CreateWatcher("https://basket")); Assert.Equal("No resolver which supports the provided service name, 'https://basket', has been configured.", exception.Message); } @@ -36,7 +36,7 @@ public async Task ServiceEndPointResolver_NoResolversConfigured_Throws() var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = new ServiceEndPointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceEndPointResolverOptions())); + var resolverFactory = new ServiceEndPointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceDiscoveryOptions())); var exception = Assert.Throws(resolverFactory.Start); Assert.Equal("No service endpoint resolvers are configured.", exception.Message); exception = await Assert.ThrowsAsync(async () => await resolverFactory.GetEndPointsAsync()); @@ -49,35 +49,34 @@ public void ResolveServiceEndPoint_NullServiceName_Throws() var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - Assert.Throws(() => resolverFactory.CreateResolver(null!)); + var resolverFactory = services.GetRequiredService(); + Assert.Throws(() => resolverFactory.CreateWatcher(null!)); } [Fact] - public async Task UseServiceDiscovery_NoResolvers_Throws() + public async Task AddServiceDiscovery_NoResolvers_Throws() { var serviceCollection = new ServiceCollection(); - serviceCollection.AddHttpClient("foo", c => c.BaseAddress = new("http://foo")) - .UseServiceDiscovery(); + serviceCollection.AddHttpClient("foo", c => c.BaseAddress = new("http://foo")).AddServiceDiscovery(); var services = serviceCollection.BuildServiceProvider(); var client = services.GetRequiredService().CreateClient("foo"); var exception = await Assert.ThrowsAsync(async () => await client.GetStringAsync("/")); Assert.Equal("No resolver which supports the provided service name, 'http://foo', has been configured.", exception.Message); } - private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointResolverProvider + private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointProviderFactory { - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { bool result; - (result, resolver) = createResolverDelegate(serviceName); + (result, resolver) = createResolverDelegate(query); return result; } } - private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointProvider + private sealed class FakeEndPointResolver(Func resolveAsync, Func disposeAsync) : IServiceEndPointProvider { - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); + public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); public ValueTask DisposeAsync() => disposeAsync(); } @@ -101,18 +100,18 @@ public async Task ResolveServiceEndPoint() disposeAsync: () => default); var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); - var initialEndPoints = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); - Assert.NotNull(initialEndPoints); - var sep = Assert.Single(initialEndPoints); + var initialResult = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(initialResult); + var sep = Assert.Single(initialResult.EndPoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); @@ -124,10 +123,9 @@ public async Task ResolveServiceEndPoint() cts[0].Cancel(); var resolverResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(resolverResult); - Assert.Equal(ResolutionStatus.Success, resolverResult.Status); Assert.True(resolverResult.ResolvedSuccessfully); - Assert.Equal(2, resolverResult.EndPoints.Count); - var endpoints = resolverResult.EndPoints.Select(ep => ep.EndPoint).OfType().ToList(); + Assert.Equal(2, resolverResult.EndPointSource.EndPoints.Count); + var endpoints = resolverResult.EndPointSource.EndPoints.Select(ep => ep.EndPoint).OfType().ToList(); endpoints.Sort((l, r) => l.Port - r.Port); Assert.Equal(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080), endpoints[0]); Assert.Equal(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888), endpoints[1]); @@ -154,15 +152,15 @@ public async Task ResolveServiceEndPointOneShot() disposeAsync: () => default); var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); var resolver = services.GetRequiredService(); Assert.NotNull(resolver); - var initialEndPoints = await resolver.GetEndPointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); - Assert.NotNull(initialEndPoints); - var sep = Assert.Single(initialEndPoints); + var initialResult = await resolver.GetEndPointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(initialResult); + var sep = Assert.Single(initialResult.EndPoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); @@ -190,12 +188,11 @@ public async Task ResolveHttpServiceEndPointOneShot() disposeAsync: () => default); var fakeResolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(fakeResolverProvider) + .AddSingleton(fakeResolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var selectorProvider = services.GetRequiredService(); - var resolverProvider = services.GetRequiredService(); - await using var resolver = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, TimeProvider.System); + var resolverProvider = services.GetRequiredService(); + await using var resolver = new HttpServiceEndPointResolver(resolverProvider, services, TimeProvider.System); Assert.NotNull(resolver); var httpRequest = new HttpRequestMessage(HttpMethod.Get, "http://basket"); @@ -232,25 +229,24 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); - return ResolutionStatus.Success; }, disposeAsync: () => default); var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); var initialEndPointsTask = resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); sem.Release(1); var initialEndPoints = await initialEndPointsTask; Assert.NotNull(initialEndPoints); - Assert.Single(initialEndPoints); + Assert.Single(initialEndPoints.EndPoints); // Tell the resolver to throw on the next resolve call and then trigger a reload. throwOnNextResolve[0] = true; @@ -283,9 +279,9 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() var task = resolver.GetEndPointsAsync(CancellationToken.None); sem.Release(1); - var endPoints = await task.ConfigureAwait(false); - Assert.NotSame(initialEndPoints, endPoints); - var sep = Assert.Single(endPoints); + var result = await task.ConfigureAwait(false); + Assert.NotSame(initialEndPoints, result); + var sep = Assert.Single(result.EndPoints); var ip = Assert.IsType(sep.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); diff --git a/tests/TestingAppHost1/TestingAppHost1.ServiceDefaults/Extensions.cs b/tests/TestingAppHost1/TestingAppHost1.ServiceDefaults/Extensions.cs index ecbc1fd83b..9fa1a0e1bb 100644 --- a/tests/TestingAppHost1/TestingAppHost1.ServiceDefaults/Extensions.cs +++ b/tests/TestingAppHost1/TestingAppHost1.ServiceDefaults/Extensions.cs @@ -25,7 +25,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu http.AddStandardResilienceHandler(); // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); // Uncomment the following to restrict the allowed schemes for service discovery. diff --git a/tests/send-to-helix.proj b/tests/send-to-helix.proj index 01f89a3b86..cc328aef3c 100644 --- a/tests/send-to-helix.proj +++ b/tests/send-to-helix.proj @@ -34,6 +34,14 @@ <_TestRunCommandArguments Include="-s .runsettings" /> <_TestRunCommandArguments Include="$(_TestNameEnvVar).dll" /> <_TestRunCommandArguments Include="--ResultsDirectory:$(_HelixLogsPath)" /> + <_TestRunCommandArguments Include="--blame-hang" /> + <_TestRunCommandArguments Include="--blame-hang-dump-type" /> + <_TestRunCommandArguments Include="full" /> + <_TestRunCommandArguments Include="--blame-hang-timeout" /> + <_TestRunCommandArguments Include="10m" /> + <_TestRunCommandArguments Include="--blame-crash" /> + <_TestRunCommandArguments Include="--blame-crash-dump-type" /> + <_TestRunCommandArguments Include="full" /> diff --git a/tests/testproject/TestProject.AppHost/TestProgram.cs b/tests/testproject/TestProject.AppHost/TestProgram.cs index 9efd9b56e0..d56bf5a8d2 100644 --- a/tests/testproject/TestProject.AppHost/TestProgram.cs +++ b/tests/testproject/TestProject.AppHost/TestProgram.cs @@ -158,7 +158,7 @@ public DistributedApplication Build() public void Dispose() => App?.Dispose(); /// - /// Writes the allocatedEndpoint endpoints to the console in JSON format. + /// Writes the allocated endpoints to the console in JSON format. /// This allows for easier consumption by the external test process. /// private sealed class EndPointWriterHook : IDistributedApplicationLifecycleHook