File: Resolver\RetryTests.cs
Web Access
Project: src\tests\Microsoft.Extensions.ServiceDiscovery.Dns.Tests\Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj (Microsoft.Extensions.ServiceDiscovery.Dns.Tests)
// 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;
using System.Net.Sockets;
using Xunit;
 
namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests;
 
public class RetryTests : LoopbackDnsTestBase
{
    public RetryTests(ITestOutputHelper output) : base(output)
    {
        Options.Attempts = 3;
    }
 
    private Task SetupUdpProcessFunction(LoopbackDnsServer server, Func<LoopbackDnsResponseBuilder, Task> func)
    {
        return Task.Run(async () =>
        {
            try
            {
                while (true)
                {
                    await server.ProcessUdpRequest(func);
                }
            }
            catch (Exception ex)
            {
                Output.WriteLine($"UDP server stopped with exception: {ex}");
                // Test teardown closed the socket, ignore
            }
        });
    }
 
    private Task SetupUdpProcessFunction(Func<LoopbackDnsResponseBuilder, Task> func)
    {
        return SetupUdpProcessFunction(DnsServer, func);
    }
 
    [Fact]
    public async Task Retry_Simple_Success()
    {
        IPAddress address = IPAddress.Parse("172.213.245.111");
        string hostName = "retry-simple-success.com";
 
        int attempt = 0;
 
        Task t = SetupUdpProcessFunction(builder =>
        {
            attempt++;
            if (attempt == Options.Attempts)
            {
                builder.Answers.AddAddress(hostName, 3600, address);
            }
            else
            {
                builder.ResponseCode = QueryResponseCode.ServerFailure;
            }
            return Task.CompletedTask;
        });
 
        AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork);
 
        AddressResult res = Assert.Single(results);
        Assert.Equal(address, res.Address);
        Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt);
    }
 
    public enum PersistentErrorType
    {
        NotImplemented,
        Refused,
        MalformedResponse
    }
 
    [Theory]
    [InlineData(PersistentErrorType.NotImplemented)]
    [InlineData(PersistentErrorType.Refused)]
    [InlineData(PersistentErrorType.MalformedResponse)]
    public async Task PersistentErrorsResponseCode_FailoverToNextServer(PersistentErrorType type)
    {
        IPAddress address = IPAddress.Parse("172.213.245.111");
        string hostName = "www.persistent.com";
 
        int primaryAttempt = 0;
        int secondaryAttempt = 0;
 
        AddressResult[] results = await RunWithFallbackServerHelper(hostName,
            builder =>
            {
                primaryAttempt++;
                switch (type)
                {
                    case PersistentErrorType.NotImplemented:
                        builder.ResponseCode = QueryResponseCode.NotImplemented;
                        break;
 
                    case PersistentErrorType.Refused:
                        builder.ResponseCode = QueryResponseCode.Refused;
                        break;
 
                    case PersistentErrorType.MalformedResponse:
                        builder.ResponseCode = (QueryResponseCode)0xFF;
                        break;
                }
                return Task.CompletedTask;
            },
            builder =>
            {
                secondaryAttempt++;
                builder.Answers.AddAddress(hostName, 3600, address);
                return Task.CompletedTask;
            });
 
        Assert.Equal(1, primaryAttempt);
        Assert.Equal(1, secondaryAttempt);
 
        AddressResult res = Assert.Single(results);
        Assert.Equal(address, res.Address);
        Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt);
    }
 
    public enum DefinitveAnswerType
    {
        NoError,
        NoData,
        NameError,
    }
 
    [Theory]
    [InlineData(DefinitveAnswerType.NoError, false)]
    [InlineData(DefinitveAnswerType.NoData, false)]
    [InlineData(DefinitveAnswerType.NoData, true)]
    [InlineData(DefinitveAnswerType.NameError, false)]
    [InlineData(DefinitveAnswerType.NameError, true)]
    public async Task DefinitiveAnswers_NoRetryOrFailover(DefinitveAnswerType type, bool additionalData)
    {
        IPAddress address = IPAddress.Parse("172.213.245.111");
        string hostName = "www.retry.com";
 
        int primaryAttempt = 0;
        int secondaryAttempt = 0;
 
        AddressResult[] results = await RunWithFallbackServerHelper(hostName,
            builder =>
            {
                primaryAttempt++;
                switch (type)
                {
                    case DefinitveAnswerType.NoError:
                        builder.ResponseCode = QueryResponseCode.NoError;
                        builder.Answers.AddAddress(hostName, 3600, address);
                        break;
 
                    case DefinitveAnswerType.NoData:
                        builder.ResponseCode = QueryResponseCode.NoError;
                        break;
 
                    case DefinitveAnswerType.NameError:
                        builder.ResponseCode = QueryResponseCode.NameError;
                        break;
                }
 
                if (additionalData)
                {
                    builder.Authorities.AddStartOfAuthority(hostName, 300, "ns1.example.com", "hostmaster.example.com", 2023101001, 1, 3600, 300, 86400);
                }
 
                return Task.CompletedTask;
            },
            builder =>
            {
                secondaryAttempt++;
                builder.ResponseCode = QueryResponseCode.Refused;
                return Task.CompletedTask;
            });
 
        Assert.Equal(1, primaryAttempt);
        Assert.Equal(0, secondaryAttempt);
 
        if (type == DefinitveAnswerType.NoError)
        {
            AddressResult res = Assert.Single(results);
            Assert.Equal(address, res.Address);
            Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt);
        }
        else
        {
            Assert.Empty(results);
        }
    }
 
    [Fact]
    public async Task ExhaustedRetries_FailoverToNextServer()
    {
        IPAddress address = IPAddress.Parse("172.213.245.111");
        string hostName = "ExhaustedRetriesFailoverToNextServer";
 
        int primaryAttempt = 0;
        int secondaryAttempt = 0;
 
        AddressResult[] results = await RunWithFallbackServerHelper(hostName,
            builder =>
            {
                primaryAttempt++;
                builder.ResponseCode = QueryResponseCode.ServerFailure;
                return Task.CompletedTask;
            },
            builder =>
            {
                secondaryAttempt++;
                builder.Answers.AddAddress(hostName, 3600, address);
                return Task.CompletedTask;
            });
 
        Assert.Equal(Options.Attempts, primaryAttempt);
        Assert.Equal(1, secondaryAttempt);
 
        AddressResult res = Assert.Single(results);
        Assert.Equal(address, res.Address);
        Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt);
    }
 
    public enum TransientErrorType
    {
        Timeout,
        ServerFailure,
        // TODO: simulate NetworkErrors
    }
 
    [Theory]
    [InlineData(TransientErrorType.Timeout)]
    [InlineData(TransientErrorType.ServerFailure)]
    public async Task TransientError_RetryOnSameServer(TransientErrorType type)
    {
        IPAddress address = IPAddress.Parse("172.213.245.111");
        string hostName = "www.transient.com";
 
        int primaryAttempt = 0;
        int secondaryAttempt = 0;
 
        AddressResult[] results = await RunWithFallbackServerHelper(hostName,
            async builder =>
            {
                primaryAttempt++;
                if (primaryAttempt == 1)
                {
                    switch (type)
                    {
                        case TransientErrorType.Timeout:
                            await Task.Delay(Options.Timeout.Multiply(1.5));
                            builder.Answers.AddAddress(hostName, 3600, address);
                            break;
 
                        case TransientErrorType.ServerFailure:
                            builder.ResponseCode = QueryResponseCode.ServerFailure;
                            break;
                    }
                }
                else
                {
                    builder.Answers.AddAddress(hostName, 3600, address);
                }
            },
            builder =>
            {
                secondaryAttempt++;
                builder.ResponseCode = QueryResponseCode.Refused;
                return Task.CompletedTask;
            });
 
        Assert.Equal(2, primaryAttempt);
        Assert.Equal(0, secondaryAttempt);
 
        AddressResult res = Assert.Single(results);
        Assert.Equal(address, res.Address);
        Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt);
    }
 
    private async Task<AddressResult[]> RunWithFallbackServerHelper(string name, Func<LoopbackDnsResponseBuilder, Task> primaryHandler, Func<LoopbackDnsResponseBuilder, Task> fallbackHandler)
    {
        Task t = SetupUdpProcessFunction(primaryHandler);
        using LoopbackDnsServer fallbackServer = new LoopbackDnsServer();
        Task t2 = SetupUdpProcessFunction(fallbackServer, fallbackHandler);
 
        Options.Servers = [DnsServer.DnsEndPoint, fallbackServer.DnsEndPoint];
 
        return await Resolver.ResolveIPAddressesAsync(name, AddressFamily.InterNetwork);
    }
 
    [Fact]
    public async Task NameError_NoRetry()
    {
        int counter = 0;
        Task t = SetupUdpProcessFunction(builder =>
        {
            counter++;
            // authoritative answer that the name does not exist
            builder.ResponseCode = QueryResponseCode.NameError;
            return Task.CompletedTask;
        });
 
        AddressResult[] results = await Resolver.ResolveIPAddressesAsync("nameerror-noretry", AddressFamily.InterNetwork);
 
        Assert.Empty(results);
        Assert.Equal(1, counter);
    }
}