Repository: netty/netty
Issue: Unable to supply resolveCache to DnsNameResolverBuilder and use TTL's for other two caches

### Expected behavior

If a resolveCache is set in the DnsNameResolverBuilder the min, max and negative TTL fields must be null. However, this means the cname cache and authoritative DNS server cache will be built with default (fallback) TTLs as opposed to a TTL which the user may want to set.

I'd expect to be able to provide the resolveCache and also TTL's which should be applied to the cname and authoritative DNS server caches.  This is not possible due to the following code:

```
       if (resolveCache != null && (minTtl != null || maxTtl != null || negativeTtl != null)) {
            throw new IllegalStateException("resolveCache and TTLs are mutually exclusive");
        }

        if (authoritativeDnsServerCache != null && (minTtl != null || maxTtl != null || negativeTtl != null)) {
            throw new IllegalStateException("authoritativeDnsServerCache and TTLs are mutually exclusive");
        }
```

This means the building of the cname cache is forced to use default values (0, Integer.MAX_VALUE)

```
    private DnsCnameCache newCnameCache() {
        return new DefaultDnsCnameCache(
                intValue(minTtl, 0), intValue(maxTtl, Integer.MAX_VALUE));
    }
```

### Actual behavior

### Steps to reproduce

Use DnsNameResolverBuilder to with a supplied resolveCache and minTtl, maxTtl or negativeTtl values.

### Minimal yet complete reproducer code (or URL to code)

### Netty version

4.1.92

### JVM version (e.g. `java -version`)

### OS version (e.g. `uname -a`)


Issue comments:
You can specify a CNAME cache by calling `DnsNameResolverBuilder.cnameCache(DnsCnameCache)`, specifying `new DefaultDnsCnameCache()` with the desired min/max TTL values. Would this work for you?
Thanks for the response, Trustin. This would work, but it feels like a workaround as opposed to allowing ttl values to be set for the other caches via the ttl fields
@trustin I think it is a bit inconvenient to have configured either all types of caches or to leave them all default based on the TTLs. I prepared one change can you take a look at this #13386?

Pull request review comments:

Patch that resolved the issue:
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCache.java
index c29f7b866b9..b75db05a7da 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCache.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCache.java
@@ -123,4 +123,14 @@ public String toString() {
         return "DefaultAuthoritativeDnsServerCache(minTtl=" + minTtl + ", maxTtl=" + maxTtl + ", cached nameservers=" +
                 resolveCache.size() + ')';
     }
+
+    // Package visibility for testing purposes
+    int minTtl() {
+        return minTtl;
+    }
+
+    // Package visibility for testing purposes
+    int maxTtl() {
+        return maxTtl;
+    }
 }
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCnameCache.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCnameCache.java
index 8395a67f6bd..9f5c6d0397c 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCnameCache.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCnameCache.java
@@ -92,4 +92,14 @@ public void clear() {
     public boolean clear(String hostname) {
         return cache.clear(checkNotNull(hostname, "hostname"));
     }
+
+    // Package visibility for testing purposes
+    int minTtl() {
+        return minTtl;
+    }
+
+    // Package visibility for testing purposes
+    int maxTtl() {
+        return maxTtl;
+    }
 }
diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java
index d3bfb191dc3..7e430338ba3 100644
--- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java
+++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java
@@ -25,6 +25,8 @@
 import io.netty.resolver.ResolvedAddressTypes;
 import io.netty.util.concurrent.Future;
 import io.netty.util.internal.ObjectUtil;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
 
 import java.net.SocketAddress;
 import java.util.ArrayList;
@@ -38,6 +40,9 @@
  * A {@link DnsNameResolver} builder.
  */
 public final class DnsNameResolverBuilder {
+
+    private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsNameResolverBuilder.class);
+
     volatile EventLoop eventLoop;
     private ChannelFactory<? extends DatagramChannel> channelFactory;
     private ChannelFactory<? extends SocketChannel> socketChannelFactory;
@@ -491,11 +496,15 @@ public DnsNameResolver build() {
         }
 
         if (resolveCache != null && (minTtl != null || maxTtl != null || negativeTtl != null)) {
-            throw new IllegalStateException("resolveCache and TTLs are mutually exclusive");
+            logger.debug("resolveCache and TTLs are mutually exclusive. TTLs are ignored.");
+        }
+
+        if (cnameCache != null && (minTtl != null || maxTtl != null || negativeTtl != null)) {
+            logger.debug("cnameCache and TTLs are mutually exclusive. TTLs are ignored.");
         }
 
         if (authoritativeDnsServerCache != null && (minTtl != null || maxTtl != null || negativeTtl != null)) {
-            throw new IllegalStateException("authoritativeDnsServerCache and TTLs are mutually exclusive");
+            logger.debug("authoritativeDnsServerCache and TTLs are mutually exclusive. TTLs are ignored.");
         }
 
         DnsCache resolveCache = this.resolveCache != null ? this.resolveCache : newCache();


Test patch:
diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverBuilderTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverBuilderTest.java
new file mode 100644
index 00000000000..bfe7bfb4740
--- /dev/null
+++ b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverBuilderTest.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2023 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.resolver.dns;
+
+import io.netty.channel.EventLoop;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioDatagramChannel;
+import io.netty.handler.codec.dns.DnsRecord;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.List;
+
+import static io.netty.resolver.dns.Cache.MAX_SUPPORTED_TTL_SECS;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DnsNameResolverBuilderTest {
+    private static final EventLoopGroup GROUP = new NioEventLoopGroup(1);
+
+    private DnsNameResolverBuilder builder;
+    private DnsNameResolver resolver;
+
+    @BeforeEach
+    void setUp() {
+        builder = new DnsNameResolverBuilder(GROUP.next()).channelType(NioDatagramChannel.class);
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (resolver != null) {
+            resolver.close();
+        }
+    }
+
+    @AfterAll
+    static void shutdownEventLoopGroup() {
+        GROUP.shutdownGracefully();
+    }
+
+    @Test
+    void testDefaults() {
+        resolver = builder.build();
+
+        checkDefaultDnsCache((DefaultDnsCache) resolver.resolveCache(), MAX_SUPPORTED_TTL_SECS, 0, 0);
+
+        checkDefaultDnsCnameCache((DefaultDnsCnameCache) resolver.cnameCache(), MAX_SUPPORTED_TTL_SECS, 0);
+
+        checkDefaultAuthoritativeDnsServerCache(
+                (DefaultAuthoritativeDnsServerCache) resolver.authoritativeDnsServerCache(), MAX_SUPPORTED_TTL_SECS, 0);
+    }
+
+    @Test
+    void testCustomDnsCacheDefaultTtl() {
+        DnsCache testDnsCache = new TestDnsCache();
+        resolver = builder.resolveCache(testDnsCache).build();
+
+        assertThat(resolver.resolveCache()).isSameAs(testDnsCache);
+
+        checkDefaultDnsCnameCache((DefaultDnsCnameCache) resolver.cnameCache(), MAX_SUPPORTED_TTL_SECS, 0);
+
+        checkDefaultAuthoritativeDnsServerCache(
+                (DefaultAuthoritativeDnsServerCache) resolver.authoritativeDnsServerCache(), MAX_SUPPORTED_TTL_SECS, 0);
+    }
+
+    @Test
+    void testCustomDnsCacheCustomTtl() {
+        DnsCache testDnsCache = new TestDnsCache();
+        resolver = builder.resolveCache(testDnsCache).ttl(1, 2).negativeTtl(3).build();
+
+        assertThat(resolver.resolveCache()).isSameAs(testDnsCache);
+
+        checkDefaultDnsCnameCache((DefaultDnsCnameCache) resolver.cnameCache(), 2, 1);
+
+        checkDefaultAuthoritativeDnsServerCache(
+                (DefaultAuthoritativeDnsServerCache) resolver.authoritativeDnsServerCache(), 2, 1);
+    }
+
+    @Test
+    void testCustomDnsCnameCacheDefaultTtl() {
+        DnsCnameCache testDnsCnameCache = new TestDnsCnameCache();
+        resolver = builder.cnameCache(testDnsCnameCache).build();
+
+        checkDefaultDnsCache((DefaultDnsCache) resolver.resolveCache(), MAX_SUPPORTED_TTL_SECS, 0, 0);
+
+        assertThat(resolver.cnameCache()).isSameAs(testDnsCnameCache);
+
+        checkDefaultAuthoritativeDnsServerCache(
+                (DefaultAuthoritativeDnsServerCache) resolver.authoritativeDnsServerCache(), MAX_SUPPORTED_TTL_SECS, 0);
+    }
+
+    @Test
+    void testCustomDnsCnameCacheCustomTtl() {
+        DnsCnameCache testDnsCnameCache = new TestDnsCnameCache();
+        resolver = builder.cnameCache(testDnsCnameCache).ttl(1, 2).negativeTtl(3).build();
+
+        checkDefaultDnsCache((DefaultDnsCache) resolver.resolveCache(), 2, 1, 3);
+
+        assertThat(resolver.cnameCache()).isSameAs(testDnsCnameCache);
+
+        checkDefaultAuthoritativeDnsServerCache(
+                (DefaultAuthoritativeDnsServerCache) resolver.authoritativeDnsServerCache(), 2, 1);
+    }
+
+    @Test
+    void testCustomAuthoritativeDnsServerCacheDefaultTtl() {
+        AuthoritativeDnsServerCache testAuthoritativeDnsServerCache = new TestAuthoritativeDnsServerCache();
+        resolver = builder.authoritativeDnsServerCache(testAuthoritativeDnsServerCache).build();
+
+        checkDefaultDnsCache((DefaultDnsCache) resolver.resolveCache(), MAX_SUPPORTED_TTL_SECS, 0, 0);
+
+        checkDefaultDnsCnameCache((DefaultDnsCnameCache) resolver.cnameCache(), MAX_SUPPORTED_TTL_SECS, 0);
+
+        assertThat(resolver.authoritativeDnsServerCache()).isSameAs(testAuthoritativeDnsServerCache);
+    }
+
+    @Test
+    void testCustomAuthoritativeDnsServerCacheCustomTtl() {
+        AuthoritativeDnsServerCache testAuthoritativeDnsServerCache = new TestAuthoritativeDnsServerCache();
+        resolver = builder.authoritativeDnsServerCache(testAuthoritativeDnsServerCache)
+                .ttl(1, 2).negativeTtl(3).build();
+
+        checkDefaultDnsCache((DefaultDnsCache) resolver.resolveCache(), 2, 1, 3);
+
+        checkDefaultDnsCnameCache((DefaultDnsCnameCache) resolver.cnameCache(), 2, 1);
+
+        assertThat(resolver.authoritativeDnsServerCache()).isSameAs(testAuthoritativeDnsServerCache);
+    }
+
+    private static void checkDefaultDnsCache(DefaultDnsCache dnsCache,
+            int expectedMaxTtl, int expectedMinTtl, int expectedNegativeTtl) {
+        assertThat(dnsCache.maxTtl()).isEqualTo(expectedMaxTtl);
+        assertThat(dnsCache.minTtl()).isEqualTo(expectedMinTtl);
+        assertThat(dnsCache.negativeTtl()).isEqualTo(expectedNegativeTtl);
+    }
+
+    private static void checkDefaultDnsCnameCache(DefaultDnsCnameCache dnsCnameCache,
+            int expectedMaxTtl, int expectedMinTtl) {
+        assertThat(dnsCnameCache.maxTtl()).isEqualTo(expectedMaxTtl);
+        assertThat(dnsCnameCache.minTtl()).isEqualTo(expectedMinTtl);
+    }
+
+    private static void checkDefaultAuthoritativeDnsServerCache(
+            DefaultAuthoritativeDnsServerCache authoritativeDnsServerCache,
+            int expectedMaxTtl, int expectedMinTtl) {
+        assertThat(authoritativeDnsServerCache.maxTtl()).isEqualTo(expectedMaxTtl);
+        assertThat(authoritativeDnsServerCache.minTtl()).isEqualTo(expectedMinTtl);
+    }
+
+    private static final class TestDnsCache implements DnsCache {
+
+        @Override
+        public void clear() {
+            //no-op
+        }
+
+        @Override
+        public boolean clear(String hostname) {
+            return false;
+        }
+
+        @Override
+        public List<? extends DnsCacheEntry> get(String hostname, DnsRecord[] additional) {
+            return null;
+        }
+
+        @Override
+        public DnsCacheEntry cache(String hostname, DnsRecord[] additional, InetAddress address,
+                long originalTtl, EventLoop loop) {
+            return null;
+        }
+
+        @Override
+        public DnsCacheEntry cache(String hostname, DnsRecord[] additional, Throwable cause, EventLoop loop) {
+            return null;
+        }
+    }
+
+    private static final class TestDnsCnameCache implements DnsCnameCache {
+
+        @Override
+        public String get(String hostname) {
+            return null;
+        }
+
+        @Override
+        public void cache(String hostname, String cname, long originalTtl, EventLoop loop) {
+            //no-op
+        }
+
+        @Override
+        public void clear() {
+            //no-op
+        }
+
+        @Override
+        public boolean clear(String hostname) {
+            return false;
+        }
+    }
+
+    private static final class TestAuthoritativeDnsServerCache implements AuthoritativeDnsServerCache {
+
+        @Override
+        public DnsServerAddressStream get(String hostname) {
+            return null;
+        }
+
+        @Override
+        public void cache(String hostname, InetSocketAddress address, long originalTtl, EventLoop loop) {
+            //no-op
+        }
+
+        @Override
+        public void clear() {
+            //no-op
+        }
+
+        @Override
+        public boolean clear(String hostname) {
+            return false;
+        }
+    }
+}