< Summary

Information
Class: IceRpc.Locator.Internal.LocationResolver
Assembly: IceRpc.Locator
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Locator/Internal/LocationResolver.cs
Tag: 1856_27024993493
Line coverage
88%
Covered lines: 59
Uncovered lines: 8
Coverable lines: 67
Total lines: 219
Line coverage: 88%
Branch coverage
100%
Covered branches: 22
Total branches: 22
Branch coverage: 100%
Method coverage
100%
Covered methods: 4
Fully covered methods: 2
Total methods: 4
Method coverage: 100%
Full method coverage: 50%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ResolveAsync(...)100%11100%
PerformResolveAsync()100%222290.47%
RefreshInBackgroundAsync()100%1155.55%

File(s)

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

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using Microsoft.Extensions.Logging;
 4
 5namespace IceRpc.Locator.Internal;
 6
 7/// <summary>Provides extension methods for <see cref="ILogger" />. They are used by <see
 8/// cref="LogLocationResolverDecorator"/>.</summary>
 9internal static partial class LocatorLoggerExtensions
 10{
 11    [LoggerMessage(
 12        EventId = (int)LocationEventId.Resolved,
 13        EventName = nameof(LocationEventId.Resolved),
 14        Level = LogLevel.Debug,
 15        Message = "Resolved {LocationKind} '{Location}' = '{ServiceAddress}'")]
 16    internal static partial void LogResolved(
 17        this ILogger logger,
 18        string locationKind,
 19        Location location,
 20        ServiceAddress serviceAddress);
 21
 22    [LoggerMessage(
 23        EventId = (int)LocationEventId.FailedToResolve,
 24        EventName = nameof(LocationEventId.FailedToResolve),
 25        Level = LogLevel.Debug,
 26        Message = "Failed to resolve {LocationKind} '{Location}'")]
 27    internal static partial void LogFailedToResolve(
 28        this ILogger logger,
 29        string locationKind,
 30        Location location,
 31        Exception? exception = null);
 32
 33    [LoggerMessage(
 34        EventId = (int)LocationEventId.BackgroundRefreshFailed,
 35        EventName = nameof(LocationEventId.BackgroundRefreshFailed),
 36        Level = LogLevel.Debug,
 37        Message = "Background cache refresh failed for {LocationKind} '{Location}'")]
 38    internal static partial void LogBackgroundRefreshFailed(
 39        this ILogger logger,
 40        string locationKind,
 41        Location location,
 42        Exception exception);
 43}
 44
 45/// <summary>An implementation of <see cref="ILocationResolver" /> without a cache.</summary>
 46internal class CacheLessLocationResolver : ILocationResolver
 47{
 48    private readonly IServerAddressFinder _serverAddressFinder;
 49
 50    internal CacheLessLocationResolver(IServerAddressFinder serverAddressFinder) =>
 51        _serverAddressFinder = serverAddressFinder;
 52
 53    public ValueTask<(ServiceAddress? ServiceAddress, bool FromCache)> ResolveAsync(
 54        Location location,
 55        bool refreshCache,
 56        CancellationToken cancellationToken) => ResolveAsync(location, cancellationToken);
 57
 58    private async ValueTask<(ServiceAddress? ServiceAddress, bool FromCache)> ResolveAsync(
 59        Location location,
 60        CancellationToken cancellationToken)
 61    {
 62        ServiceAddress? serviceAddress = await _serverAddressFinder.FindAsync(location, cancellationToken)
 63            .ConfigureAwait(false);
 64
 65        // A well-known service address resolution can return a service address with an adapter ID
 66        if (serviceAddress is not null && serviceAddress.Params.TryGetValue("adapter-id", out string? escapedAdapterId))
 67        {
 68            (serviceAddress, _) = await ResolveAsync(
 69                new Location { IsAdapterId = true, Value = Uri.UnescapeDataString(escapedAdapterId) },
 70                cancellationToken).ConfigureAwait(false);
 71        }
 72
 73        return (serviceAddress, false);
 74    }
 75}
 76
 77/// <summary>The main implementation of <see cref="ILocationResolver" />, with a cache.</summary>
 78internal class LocationResolver : ILocationResolver
 79{
 80    private readonly bool _background;
 81    private readonly ILogger _logger;
 82    private readonly IServerAddressCache _serverAddressCache;
 83    private readonly IServerAddressFinder _serverAddressFinder;
 84    private readonly TimeSpan _refreshThreshold;
 85
 86    private readonly TimeSpan _ttl;
 87
 888    internal LocationResolver(
 889        IServerAddressFinder serverAddressFinder,
 890        IServerAddressCache serverAddressCache,
 891        bool background,
 892        TimeSpan refreshThreshold,
 893        TimeSpan ttl,
 894        ILogger logger)
 895    {
 896        _serverAddressFinder = serverAddressFinder;
 897        _serverAddressCache = serverAddressCache;
 898        _background = background;
 899        _refreshThreshold = refreshThreshold;
 8100        _ttl = ttl;
 8101        _logger = logger;
 8102    }
 103
 104    public ValueTask<(ServiceAddress? ServiceAddress, bool FromCache)> ResolveAsync(
 105        Location location,
 106        bool refreshCache,
 9107        CancellationToken cancellationToken) => PerformResolveAsync(location, refreshCache, cancellationToken);
 108
 109    private async ValueTask<(ServiceAddress? ServiceAddress, bool FromCache)> PerformResolveAsync(
 110        Location location,
 111        bool refreshCache,
 112        CancellationToken cancellationToken)
 11113    {
 11114        ServiceAddress? serviceAddress = null;
 11115        bool expired = false;
 11116        bool justRefreshed = false;
 11117        bool resolved = false;
 118
 11119        if (_serverAddressCache.TryGetValue(location, out (TimeSpan InsertionTime, ServiceAddress ServiceAddress) entry)
 7120        {
 7121            serviceAddress = entry.ServiceAddress;
 7122            TimeSpan cacheEntryAge = TimeSpan.FromMilliseconds(Environment.TickCount64) - entry.InsertionTime;
 7123            expired = _ttl != Timeout.InfiniteTimeSpan && cacheEntryAge > _ttl;
 7124            justRefreshed = cacheEntryAge <= _refreshThreshold;
 7125        }
 126
 11127        if (serviceAddress is null || (!_background && expired) || (refreshCache && !justRefreshed))
 6128        {
 6129            serviceAddress = await _serverAddressFinder.FindAsync(location, cancellationToken).ConfigureAwait(false);
 6130            resolved = true;
 6131        }
 5132        else if (_background && expired)
 1133        {
 134            // We retrieved an expired service address from the cache, so we launch a refresh in the background.
 1135            _ = RefreshInBackgroundAsync();
 1136        }
 137
 138        // A well-known service address resolution can return a service address with an adapter-id.
 11139        if (serviceAddress is not null && serviceAddress.Params.TryGetValue("adapter-id", out string? escapedAdapterId))
 2140        {
 141            try
 2142            {
 143                // Resolves adapter ID recursively, by checking first the cache. If we resolved the well-known
 144                // service address, we request a cache refresh for the adapter ID.
 2145                (serviceAddress, _) = await PerformResolveAsync(
 2146                    new Location { IsAdapterId = true, Value = Uri.UnescapeDataString(escapedAdapterId) },
 2147                    refreshCache || resolved,
 2148                    cancellationToken).ConfigureAwait(false);
 2149            }
 0150            catch
 0151            {
 0152                serviceAddress = null;
 0153                throw;
 154            }
 155            finally
 2156            {
 157                // When the second resolution fails, we clear the cache entry for the initial successful
 158                // resolution, since the overall resolution is a failure.
 2159                if (serviceAddress is null)
 1160                {
 1161                    _serverAddressCache.Remove(location);
 1162                }
 2163            }
 2164        }
 165
 11166        return (serviceAddress, serviceAddress is not null && !resolved);
 167
 168        async Task RefreshInBackgroundAsync()
 1169        {
 170            try
 1171            {
 1172                _ = await _serverAddressFinder.FindAsync(location, cancellationToken: default).ConfigureAwait(false);
 1173            }
 0174            catch (Exception exception)
 0175            {
 0176                _logger.LogBackgroundRefreshFailed(location.Kind, location, exception);
 0177            }
 1178        }
 11179    }
 180}
 181
 182/// <summary>A decorator that adds event source logging to a location resolver.</summary>
 183internal class LogLocationResolverDecorator : ILocationResolver
 184{
 185    private readonly ILocationResolver _decoratee;
 186    private readonly ILogger _logger;
 187
 188    public async ValueTask<(ServiceAddress? ServiceAddress, bool FromCache)> ResolveAsync(
 189        Location location,
 190        bool refreshCache,
 191        CancellationToken cancellationToken)
 192    {
 193        try
 194        {
 195            (ServiceAddress? serviceAddress, bool fromCache) =
 196                await _decoratee.ResolveAsync(location, refreshCache, cancellationToken).ConfigureAwait(false);
 197            if (serviceAddress is not null)
 198            {
 199                _logger.LogResolved(location.Kind, location, serviceAddress);
 200            }
 201            else
 202            {
 203                _logger.LogFailedToResolve(location.Kind, location);
 204            }
 205            return (serviceAddress, fromCache);
 206        }
 207        catch (Exception exception)
 208        {
 209            _logger.LogFailedToResolve(location.Kind, location, exception);
 210            throw;
 211        }
 212    }
 213
 214    internal LogLocationResolverDecorator(ILocationResolver decoratee, ILogger logger)
 215    {
 216        _decoratee = decoratee;
 217        _logger = logger;
 218    }
 219}