< Summary

Information
Class: IceRpc.Locator.LocatorLocationResolver
Assembly: IceRpc.Locator
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Locator/LocatorInterceptor.cs
Tag: 1856_27024993493
Line coverage
75%
Covered lines: 28
Uncovered lines: 9
Coverable lines: 37
Total lines: 242
Line coverage: 75.6%
Branch coverage
80%
Covered branches: 16
Total branches: 20
Branch coverage: 80%
Method coverage
100%
Covered methods: 2
Fully covered methods: 1
Total methods: 2
Method coverage: 100%
Full method coverage: 50%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)80%262075%
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.Ice;
 5using IceRpc.Locator.Internal;
 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? escapedAdapterId) ?
 61                    new Location { IsAdapterId = true, Value = Uri.UnescapeDataString(escapedAdapterId) } :
 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            // Use the ServerAddressComparer.OptionalTransport comparer so the filter matches the connection layer's
 112            // equality.
 113            (ServerAddress? ServerAddress, ImmutableList<ServerAddress> AltServerAddresses) result =
 114                (serviceAddress.ServerAddress, serviceAddress.AltServerAddresses);
 115            if (result.ServerAddress is ServerAddress serverAddress &&
 116                excludedAddresses.Contains(serverAddress, ServerAddressComparer.OptionalTransport))
 117            {
 118                result.ServerAddress = null;
 119            }
 120            result.AltServerAddresses = result.AltServerAddresses.RemoveAll(
 121                e => excludedAddresses.Contains(e, ServerAddressComparer.OptionalTransport));
 122
 123            if (result.ServerAddress is null && result.AltServerAddresses.Count > 0)
 124            {
 125                result.ServerAddress = result.AltServerAddresses[0];
 126                result.AltServerAddresses = result.AltServerAddresses.RemoveAt(0);
 127            }
 128            return result;
 129        }
 130    }
 131
 132    private interface ICachedResolutionFeature
 133    {
 134        Location Location { get; }
 135    }
 136
 137    private class CachedResolutionFeature : ICachedResolutionFeature
 138    {
 139        public Location Location { get; }
 140
 141        internal CachedResolutionFeature(Location location) => Location = location;
 142    }
 143}
 144
 145/// <summary>A location is either an adapter ID or a path.</summary>
 146public readonly record struct Location
 147{
 148    /// <summary>Gets a value indicating whether or not this location holds an adapter ID; otherwise,
 149    /// <see langword="false" />.</summary>
 150    public bool IsAdapterId { get; init; }
 151
 152    /// <summary>Gets the adapter ID or path.</summary>
 153    public required string Value { get; init; }
 154
 155    internal string Kind => IsAdapterId ? "adapter ID" : "well-known service address";
 156
 157    /// <summary>Returns <see cref="Value"/>.</summary>
 158    /// <returns>The adapter ID or path.</returns>
 159    public override string ToString() => Value;
 160}
 161
 162/// <summary>A location resolver resolves a location into one or more server addresses carried by a dummy service
 163/// address, and optionally maintains a cache for these resolutions. It's the "brain" of
 164/// <see cref="LocatorInterceptor" />. The same location resolver can be shared by multiple locator interceptors.
 165/// </summary>
 166public interface ILocationResolver
 167{
 168    /// <summary>Resolves a location into one or more server addresses carried by a dummy service address.</summary>
 169    /// <param name="location">The location to resolve.</param>
 170    /// <param name="refreshCache">When <see langword="true" />, requests a cache refresh.</param>
 171    /// <param name="cancellationToken">The cancellation token.</param>
 172    /// <returns>A tuple with a nullable dummy service address that holds the server addresses (if resolved), and a bool
 173    /// that indicates whether these server addresses were retrieved from the implementation's cache. ServiceAddress is
 174    /// <see langword="null" /> when the location resolver fails to resolve a location. When ServiceAddress is not null,
 175    /// its ServerAddress is not <see langword="null" />.</returns>
 176    ValueTask<(ServiceAddress? ServiceAddress, bool FromCache)> ResolveAsync(
 177        Location location,
 178        bool refreshCache,
 179        CancellationToken cancellationToken);
 180}
 181
 182/// <summary>Implements <see cref="ILocationResolver" /> using an <see cref="ILocator"/>.</summary>
 183public class LocatorLocationResolver : ILocationResolver
 184{
 185    private readonly ILocationResolver _locationResolver;
 186
 187    /// <summary>Constructs a locator location resolver.</summary>
 188    /// <param name="locator">The locator.</param>
 189    /// <param name="options">The locator options.</param>
 190    /// <param name="logger">The logger.</param>
 3191    public LocatorLocationResolver(ILocator locator, LocatorOptions options, ILogger logger)
 3192    {
 193        // This is the composition root of this locator location resolver.
 3194        if (options.Ttl != Timeout.InfiniteTimeSpan && options.RefreshThreshold >= options.Ttl)
 1195        {
 1196            throw new InvalidOperationException(
 1197                $"The value of {nameof(options.RefreshThreshold)} must be smaller than the value of {nameof(options.Ttl)
 198        }
 199
 200        // Create and decorate server address cache (if caching enabled):
 2201        IServerAddressCache? serverAddressCache = options.Ttl != TimeSpan.Zero && options.MaxCacheSize > 0 ?
 2202            new ServerAddressCache(options.MaxCacheSize) : null;
 2203        if (serverAddressCache is not null && logger != NullLogger.Instance)
 0204        {
 0205            serverAddressCache = new LogServerAddressCacheDecorator(serverAddressCache, logger);
 0206        }
 207
 208        // Create and decorate server address finder:
 2209        IServerAddressFinder serverAddressFinder = new LocatorServerAddressFinder(locator);
 2210        if (logger != NullLogger.Instance)
 0211        {
 0212            serverAddressFinder = new LogServerAddressFinderDecorator(serverAddressFinder, logger);
 0213        }
 214
 2215        if (serverAddressCache is not null)
 1216        {
 1217            serverAddressFinder = new CacheUpdateServerAddressFinderDecorator(serverAddressFinder, serverAddressCache);
 1218        }
 2219        serverAddressFinder = new CoalesceServerAddressFinderDecorator(serverAddressFinder, options.ResolveTimeout);
 220
 2221        _locationResolver = serverAddressCache is null ?
 2222            new CacheLessLocationResolver(serverAddressFinder) :
 2223            new LocationResolver(
 2224                serverAddressFinder,
 2225                serverAddressCache,
 2226                options.Background,
 2227                options.RefreshThreshold,
 2228                options.Ttl,
 2229                logger);
 2230        if (logger != NullLogger.Instance)
 0231        {
 0232            _locationResolver = new LogLocationResolverDecorator(_locationResolver, logger);
 0233        }
 2234    }
 235
 236    /// <inheritdoc/>
 237    public ValueTask<(ServiceAddress? ServiceAddress, bool FromCache)> ResolveAsync(
 238        Location location,
 239        bool refreshCache,
 240        CancellationToken cancellationToken) =>
 4241        _locationResolver.ResolveAsync(location, refreshCache, cancellationToken);
 242}