< Summary

Information
Class: IceRpc.Locator.LocatorInterceptor
Assembly: IceRpc.Locator
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Locator/LocatorInterceptor.cs
Tag: 275_13775359185
Line coverage
90%
Covered lines: 66
Uncovered lines: 7
Coverable lines: 73
Total lines: 237
Line coverage: 90.4%
Branch coverage
81%
Covered branches: 26
Total branches: 32
Branch coverage: 81.2%
Method coverage
100%
Covered methods: 5
Total methods: 5
Method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
InvokeAsync()91.66%2424100%
ComputeServerAddresses()50%14.51853.33%
get_Location()100%11100%
.ctor(...)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>
 528    public LocatorInterceptor(IInvoker next, ILocationResolver locationResolver)
 529    {
 530        _next = next;
 531        _locationResolver = locationResolver;
 532    }
 33
 34    /// <inheritdoc/>
 35    public async Task<IncomingResponse> InvokeAsync(OutgoingRequest request, CancellationToken cancellationToken)
 736    {
 737        if (request.Protocol == Protocol.Ice && request.ServiceAddress.ServerAddress is null)
 638        {
 639            Location location = default;
 640            bool refreshCache = false;
 41
 642            if (request.Features.Get<IServerAddressFeature>() is not IServerAddressFeature serverAddressFeature)
 443            {
 444                serverAddressFeature = new ServerAddressFeature(request.ServiceAddress);
 445                request.Features = request.Features.With(serverAddressFeature);
 446            }
 47
 48            // We detect retries and don't use cached values for retries by setting refreshCache to true.
 49
 650            if (request.Features.Get<ICachedResolutionFeature>() is ICachedResolutionFeature cachedResolution)
 151            {
 52                // This is the second (or greater) attempt, and we provided a cached resolution with the
 53                // first attempt and all subsequent attempts.
 54
 155                location = cachedResolution.Location;
 156                refreshCache = true;
 157            }
 558            else if (serverAddressFeature.ServerAddress is null)
 459            {
 460                location = request.ServiceAddress.Params.TryGetValue("adapter-id", out string? adapterId) ?
 461                    new Location { IsAdapterId = true, Value = adapterId } :
 462                    new Location { Value = request.ServiceAddress.Path };
 463            }
 64            // else it could be a retry where the first attempt provided non-cached server address(es)
 65
 666            if (location != default)
 567            {
 568                (ServiceAddress? serviceAddress, bool fromCache) = await _locationResolver.ResolveAsync(
 569                    location,
 570                    refreshCache,
 571                    cancellationToken).ConfigureAwait(false);
 72
 573                if (refreshCache)
 174                {
 175                    if (!fromCache && !request.Features.IsReadOnly)
 176                    {
 77                        // No need to resolve this location again since we are not returning a cached value.
 178                        request.Features.Set<ICachedResolutionFeature>(null);
 179                    }
 180                }
 481                else if (fromCache)
 182                {
 83                    // Make sure the next attempt re-resolves location and sets refreshCache to true.
 184                    request.Features = request.Features.With<ICachedResolutionFeature>(
 185                        new CachedResolutionFeature(location));
 186                }
 87
 588                if (serviceAddress is not null)
 589                {
 90                    // A well behaved location resolver should never return a non-null service address with a null
 91                    // serverAddress.
 592                    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.
 597                    (ServerAddress? serverAddress, ImmutableList<ServerAddress> altServerAddresses) =
 598                        ComputeServerAddresses(serviceAddress, serverAddressFeature.RemovedServerAddresses);
 599                    serverAddressFeature.ServerAddress = serverAddress;
 5100                    serverAddressFeature.AltServerAddresses = altServerAddresses;
 5101                }
 102                // else, resolution failed and we don't update anything
 5103            }
 6104        }
 7105        return await _next.InvokeAsync(request, cancellationToken).ConfigureAwait(false);
 106
 107        static (ServerAddress? ServerAddress, ImmutableList<ServerAddress> AltServerAddresses) ComputeServerAddresses(
 108            ServiceAddress serviceAddress,
 109            IEnumerable<ServerAddress> excludedAddresses)
 5110        {
 5111            (ServerAddress? ServerAddress, ImmutableList<ServerAddress> AltServerAddresses) result =
 5112                (serviceAddress.ServerAddress, serviceAddress.AltServerAddresses);
 5113            if (result.ServerAddress is ServerAddress serverAddress && excludedAddresses.Contains(serverAddress))
 0114            {
 0115                result.ServerAddress = null;
 0116            }
 5117            result.AltServerAddresses = result.AltServerAddresses.RemoveAll(e => excludedAddresses.Contains(e));
 118
 5119            if (result.ServerAddress is null && result.AltServerAddresses.Count > 0)
 0120            {
 0121                result.ServerAddress = result.AltServerAddresses[0];
 0122                result.AltServerAddresses = result.AltServerAddresses.RemoveAt(0);
 0123            }
 5124            return result;
 5125        }
 7126    }
 127
 128    private interface ICachedResolutionFeature
 129    {
 130        Location Location { get; }
 131    }
 132
 133    private class CachedResolutionFeature : ICachedResolutionFeature
 134    {
 1135        public Location Location { get; }
 136
 2137        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>
 187    public LocatorLocationResolver(ILocator locator, LocatorOptions options, ILogger logger)
 188    {
 189        // This is the composition root of this locator location resolver.
 190        if (options.Ttl != Timeout.InfiniteTimeSpan && options.RefreshThreshold >= options.Ttl)
 191        {
 192            throw new InvalidOperationException(
 193                $"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):
 197        IServerAddressCache? serverAddressCache = options.Ttl != TimeSpan.Zero && options.MaxCacheSize > 0 ?
 198            new ServerAddressCache(options.MaxCacheSize) : null;
 199        if (serverAddressCache is not null && logger != NullLogger.Instance)
 200        {
 201            serverAddressCache = new LogServerAddressCacheDecorator(serverAddressCache, logger);
 202        }
 203
 204        // Create and decorate server address finder:
 205        IServerAddressFinder serverAddressFinder = new LocatorServerAddressFinder(locator);
 206        if (logger != NullLogger.Instance)
 207        {
 208            serverAddressFinder = new LogServerAddressFinderDecorator(serverAddressFinder, logger);
 209        }
 210
 211        if (serverAddressCache is not null)
 212        {
 213            serverAddressFinder = new CacheUpdateServerAddressFinderDecorator(serverAddressFinder, serverAddressCache);
 214        }
 215        serverAddressFinder = new CoalesceServerAddressFinderDecorator(serverAddressFinder);
 216
 217        _locationResolver = serverAddressCache is null ?
 218            new CacheLessLocationResolver(serverAddressFinder) :
 219            new LocationResolver(
 220                serverAddressFinder,
 221                serverAddressCache,
 222                options.Background,
 223                options.RefreshThreshold,
 224                options.Ttl);
 225        if (logger != NullLogger.Instance)
 226        {
 227            _locationResolver = new LogLocationResolverDecorator(_locationResolver, logger);
 228        }
 229    }
 230
 231    /// <inheritdoc/>
 232    public ValueTask<(ServiceAddress? ServiceAddress, bool FromCache)> ResolveAsync(
 233        Location location,
 234        bool refreshCache,
 235        CancellationToken cancellationToken) =>
 236        _locationResolver.ResolveAsync(location, refreshCache, cancellationToken);
 237}