< Summary

Information
Class: IceRpc.Locator.LocatorLocationResolver
Assembly: IceRpc.Locator
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Locator/LocatorInterceptor.cs
Tag: 275_13775359185
Line coverage
75%
Covered lines: 27
Uncovered lines: 9
Coverable lines: 36
Total lines: 237
Line coverage: 75%
Branch coverage
85%
Covered branches: 17
Total branches: 20
Branch coverage: 85%
Method coverage
100%
Covered methods: 2
Total methods: 2
Method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)85%26.812074.28%
ResolveAsync(...)100%11100%

File(s)

/home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Locator/LocatorInterceptor.cs

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using IceRpc.Features;
 4using IceRpc.Locator.Internal;
 5using IceRpc.Slice.Ice;
 6using Microsoft.Extensions.Logging;
 7using Microsoft.Extensions.Logging.Abstractions;
 8using System.Collections.Immutable;
 9using System.Diagnostics;
 10
 11namespace IceRpc.Locator;
 12
 13/// <summary>A locator interceptor intercepts ice requests that have no server address and attempts to assign a usable
 14/// server address (and alt-server addresses) to such requests via the <see cref="IServerAddressFeature" />. You would
 15/// usually install the retry interceptor before the locator interceptor in the invocation pipeline and use the
 16/// connection cache invoker for the pipeline, with this setup the locator interceptor would be able to detect
 17/// invocation retries and refreshes the server address when required, and the connection cache would take care of
 18/// creating the connections for the resolved server address.</summary>
 19public class LocatorInterceptor : IInvoker
 20{
 21    private readonly IInvoker _next;
 22    private readonly ILocationResolver _locationResolver;
 23
 24    /// <summary>Constructs a locator interceptor.</summary>
 25    /// <param name="next">The next invoker in the invocation pipeline.</param>
 26    /// <param name="locationResolver">The location resolver. It is usually a <see cref="LocatorLocationResolver" />.
 27    /// </param>
 28    public LocatorInterceptor(IInvoker next, ILocationResolver locationResolver)
 29    {
 30        _next = next;
 31        _locationResolver = locationResolver;
 32    }
 33
 34    /// <inheritdoc/>
 35    public async Task<IncomingResponse> InvokeAsync(OutgoingRequest request, CancellationToken cancellationToken)
 36    {
 37        if (request.Protocol == Protocol.Ice && request.ServiceAddress.ServerAddress is null)
 38        {
 39            Location location = default;
 40            bool refreshCache = false;
 41
 42            if (request.Features.Get<IServerAddressFeature>() is not IServerAddressFeature serverAddressFeature)
 43            {
 44                serverAddressFeature = new ServerAddressFeature(request.ServiceAddress);
 45                request.Features = request.Features.With(serverAddressFeature);
 46            }
 47
 48            // We detect retries and don't use cached values for retries by setting refreshCache to true.
 49
 50            if (request.Features.Get<ICachedResolutionFeature>() is ICachedResolutionFeature cachedResolution)
 51            {
 52                // This is the second (or greater) attempt, and we provided a cached resolution with the
 53                // first attempt and all subsequent attempts.
 54
 55                location = cachedResolution.Location;
 56                refreshCache = true;
 57            }
 58            else if (serverAddressFeature.ServerAddress is null)
 59            {
 60                location = request.ServiceAddress.Params.TryGetValue("adapter-id", out string? adapterId) ?
 61                    new Location { IsAdapterId = true, Value = adapterId } :
 62                    new Location { Value = request.ServiceAddress.Path };
 63            }
 64            // else it could be a retry where the first attempt provided non-cached server address(es)
 65
 66            if (location != default)
 67            {
 68                (ServiceAddress? serviceAddress, bool fromCache) = await _locationResolver.ResolveAsync(
 69                    location,
 70                    refreshCache,
 71                    cancellationToken).ConfigureAwait(false);
 72
 73                if (refreshCache)
 74                {
 75                    if (!fromCache && !request.Features.IsReadOnly)
 76                    {
 77                        // No need to resolve this location again since we are not returning a cached value.
 78                        request.Features.Set<ICachedResolutionFeature>(null);
 79                    }
 80                }
 81                else if (fromCache)
 82                {
 83                    // Make sure the next attempt re-resolves location and sets refreshCache to true.
 84                    request.Features = request.Features.With<ICachedResolutionFeature>(
 85                        new CachedResolutionFeature(location));
 86                }
 87
 88                if (serviceAddress is not null)
 89                {
 90                    // A well behaved location resolver should never return a non-null service address with a null
 91                    // serverAddress.
 92                    Debug.Assert(serviceAddress.ServerAddress is not null);
 93
 94                    // Before assigning the new resolved server addresses to the server address feature we have to
 95                    // remove any server addresses that are included in the list of removed server addresses, to
 96                    // avoid retrying with a server address that has been already excluded for the invocation.
 97                    (ServerAddress? serverAddress, ImmutableList<ServerAddress> altServerAddresses) =
 98                        ComputeServerAddresses(serviceAddress, serverAddressFeature.RemovedServerAddresses);
 99                    serverAddressFeature.ServerAddress = serverAddress;
 100                    serverAddressFeature.AltServerAddresses = altServerAddresses;
 101                }
 102                // else, resolution failed and we don't update anything
 103            }
 104        }
 105        return await _next.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
 106
 107        static (ServerAddress? ServerAddress, ImmutableList<ServerAddress> AltServerAddresses) ComputeServerAddresses(
 108            ServiceAddress serviceAddress,
 109            IEnumerable<ServerAddress> excludedAddresses)
 110        {
 111            (ServerAddress? ServerAddress, ImmutableList<ServerAddress> AltServerAddresses) result =
 112                (serviceAddress.ServerAddress, serviceAddress.AltServerAddresses);
 113            if (result.ServerAddress is ServerAddress serverAddress && excludedAddresses.Contains(serverAddress))
 114            {
 115                result.ServerAddress = null;
 116            }
 117            result.AltServerAddresses = result.AltServerAddresses.RemoveAll(e => excludedAddresses.Contains(e));
 118
 119            if (result.ServerAddress is null && result.AltServerAddresses.Count > 0)
 120            {
 121                result.ServerAddress = result.AltServerAddresses[0];
 122                result.AltServerAddresses = result.AltServerAddresses.RemoveAt(0);
 123            }
 124            return result;
 125        }
 126    }
 127
 128    private interface ICachedResolutionFeature
 129    {
 130        Location Location { get; }
 131    }
 132
 133    private class CachedResolutionFeature : ICachedResolutionFeature
 134    {
 135        public Location Location { get; }
 136
 137        internal CachedResolutionFeature(Location location) => Location = location;
 138    }
 139}
 140
 141/// <summary>A location is either an adapter ID or a path.</summary>
 142public readonly record struct Location
 143{
 144    /// <summary>Gets a value indicating whether or not this location holds an adapter ID; otherwise,
 145    /// <see langword="false" />.</summary>
 146    public bool IsAdapterId { get; init; }
 147
 148    /// <summary>Gets the adapter ID or path.</summary>
 149    public required string Value { get; init; }
 150
 151    internal string Kind => IsAdapterId ? "adapter ID" : "well-known service address";
 152
 153    /// <summary>Returns <see cref="Value"/>.</summary>
 154    /// <returns>The adapter ID or path.</returns>
 155    public override string ToString() => Value;
 156}
 157
 158/// <summary>A location resolver resolves a location into one or more server addresses carried by a dummy service
 159/// address, and optionally maintains a cache for these resolutions. It's the "brain" of
 160/// <see cref="LocatorInterceptor" />. The same location resolver can be shared by multiple locator interceptors.
 161/// </summary>
 162public interface ILocationResolver
 163{
 164    /// <summary>Resolves a location into one or more server addresses carried by a dummy service address.</summary>
 165    /// <param name="location">The location to resolve.</param>
 166    /// <param name="refreshCache">When <see langword="true" />, requests a cache refresh.</param>
 167    /// <param name="cancellationToken">The cancellation token.</param>
 168    /// <returns>A tuple with a nullable dummy service address that holds the server addresses (if resolved), and a bool
 169    /// that indicates whether these server addresses were retrieved from the implementation's cache. ServiceAddress is
 170    /// <see langword="null" /> when the location resolver fails to resolve a location. When ServiceAddress is not null,
 171    /// its ServerAddress is not <see langword="null" />.</returns>
 172    ValueTask<(ServiceAddress? ServiceAddress, bool FromCache)> ResolveAsync(
 173        Location location,
 174        bool refreshCache,
 175        CancellationToken cancellationToken);
 176}
 177
 178/// <summary>Implements <see cref="ILocationResolver" /> using an <see cref="ILocator"/>.</summary>
 179public class LocatorLocationResolver : ILocationResolver
 180{
 181    private readonly ILocationResolver _locationResolver;
 182
 183    /// <summary>Constructs a locator location resolver.</summary>
 184    /// <param name="locator">The locator.</param>
 185    /// <param name="options">The locator options.</param>
 186    /// <param name="logger">The logger.</param>
 3187    public LocatorLocationResolver(ILocator locator, LocatorOptions options, ILogger logger)
 3188    {
 189        // This is the composition root of this locator location resolver.
 3190        if (options.Ttl != Timeout.InfiniteTimeSpan && options.RefreshThreshold >= options.Ttl)
 1191        {
 1192            throw new InvalidOperationException(
 1193                $"The value of {nameof(options.RefreshThreshold)} must be smaller than the value of {nameof(options.Ttl)
 194        }
 195
 196        // Create and decorate server address cache (if caching enabled):
 2197        IServerAddressCache? serverAddressCache = options.Ttl != TimeSpan.Zero && options.MaxCacheSize > 0 ?
 2198            new ServerAddressCache(options.MaxCacheSize) : null;
 2199        if (serverAddressCache is not null && logger != NullLogger.Instance)
 0200        {
 0201            serverAddressCache = new LogServerAddressCacheDecorator(serverAddressCache, logger);
 0202        }
 203
 204        // Create and decorate server address finder:
 2205        IServerAddressFinder serverAddressFinder = new LocatorServerAddressFinder(locator);
 2206        if (logger != NullLogger.Instance)
 0207        {
 0208            serverAddressFinder = new LogServerAddressFinderDecorator(serverAddressFinder, logger);
 0209        }
 210
 2211        if (serverAddressCache is not null)
 1212        {
 1213            serverAddressFinder = new CacheUpdateServerAddressFinderDecorator(serverAddressFinder, serverAddressCache);
 1214        }
 2215        serverAddressFinder = new CoalesceServerAddressFinderDecorator(serverAddressFinder);
 216
 2217        _locationResolver = serverAddressCache is null ?
 2218            new CacheLessLocationResolver(serverAddressFinder) :
 2219            new LocationResolver(
 2220                serverAddressFinder,
 2221                serverAddressCache,
 2222                options.Background,
 2223                options.RefreshThreshold,
 2224                options.Ttl);
 2225        if (logger != NullLogger.Instance)
 0226        {
 0227            _locationResolver = new LogLocationResolverDecorator(_locationResolver, logger);
 0228        }
 2229    }
 230
 231    /// <inheritdoc/>
 232    public ValueTask<(ServiceAddress? ServiceAddress, bool FromCache)> ResolveAsync(
 233        Location location,
 234        bool refreshCache,
 235        CancellationToken cancellationToken) =>
 4236        _locationResolver.ResolveAsync(location, refreshCache, cancellationToken);
 237}