Skip to content

Commit d05af24

Browse files
qnnnnormanmaurerbryce-anderson
authored
Fix potential DNS cache invalidation across different EventLoops (#14147)
Motivation: By default, `DnsAddressResolverGroup` sets up a separate `DNSCache` for each `EventLoop`. This can lead to cache invalidation when performing DNS resolution across different EventLoops due to the potential absence of cached DNS records. Modification: - initializing the DNSCache in `DnsNameResolverBuilder` during `DnsAddressResolverGroup` initialization to enable sharing of DNSCache among EventLoops under the same `DnsAddressResolverGroup`. - Add unit test: testSharedDNSCacheAcrossEventLoops Result: Fixes #14046. --------- Co-authored-by: Norman Maurer <[email protected]> Co-authored-by: Bryce Anderson <[email protected]>
1 parent d1f22c2 commit d05af24

File tree

3 files changed

+77
-10
lines changed

3 files changed

+77
-10
lines changed

resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,31 @@ public class DnsAddressResolverGroup extends AddressResolverGroup<InetSocketAddr
4545
private final ConcurrentMap<String, Promise<List<InetAddress>>> resolveAllsInProgress = newConcurrentHashMap();
4646

4747
public DnsAddressResolverGroup(DnsNameResolverBuilder dnsResolverBuilder) {
48-
this.dnsResolverBuilder = dnsResolverBuilder.copy();
48+
this.dnsResolverBuilder = withSharedCaches(dnsResolverBuilder.copy());
4949
}
5050

5151
public DnsAddressResolverGroup(
5252
Class<? extends DatagramChannel> channelType,
5353
DnsServerAddressStreamProvider nameServerProvider) {
54-
this.dnsResolverBuilder = new DnsNameResolverBuilder();
54+
this.dnsResolverBuilder = withSharedCaches(new DnsNameResolverBuilder());
5555
dnsResolverBuilder.channelType(channelType).nameServerProvider(nameServerProvider);
5656
}
5757

5858
public DnsAddressResolverGroup(
5959
ChannelFactory<? extends DatagramChannel> channelFactory,
6060
DnsServerAddressStreamProvider nameServerProvider) {
61-
this.dnsResolverBuilder = new DnsNameResolverBuilder();
61+
this.dnsResolverBuilder = withSharedCaches(new DnsNameResolverBuilder());
6262
dnsResolverBuilder.channelFactory(channelFactory).nameServerProvider(nameServerProvider);
6363
}
6464

65+
private static DnsNameResolverBuilder withSharedCaches(DnsNameResolverBuilder dnsResolverBuilder) {
66+
/// To avoid each member of the group having its own cache we either use the configured cache
67+
// or create a new one to share among the entire group.
68+
return dnsResolverBuilder.resolveCache(dnsResolverBuilder.getOrNewCache())
69+
.cnameCache(dnsResolverBuilder.getOrNewCnameCache())
70+
.authoritativeDnsServerCache(dnsResolverBuilder.getOrNewAuthoritativeDnsServerCache());
71+
}
72+
6573
@SuppressWarnings("deprecation")
6674
@Override
6775
protected final AddressResolver<InetSocketAddress> newResolver(EventExecutor executor) throws Exception {

resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java

+15-7
Original file line numberDiff line numberDiff line change
@@ -513,11 +513,17 @@ public DnsNameResolverBuilder ndots(int ndots) {
513513
return this;
514514
}
515515

516-
private DnsCache newCache() {
516+
DnsCache getOrNewCache() {
517+
if (this.resolveCache != null) {
518+
return this.resolveCache;
519+
}
517520
return new DefaultDnsCache(intValue(minTtl, 0), intValue(maxTtl, Integer.MAX_VALUE), intValue(negativeTtl, 0));
518521
}
519522

520-
private AuthoritativeDnsServerCache newAuthoritativeDnsServerCache() {
523+
AuthoritativeDnsServerCache getOrNewAuthoritativeDnsServerCache() {
524+
if (this.authoritativeDnsServerCache != null) {
525+
return this.authoritativeDnsServerCache;
526+
}
521527
return new DefaultAuthoritativeDnsServerCache(
522528
intValue(minTtl, 0), intValue(maxTtl, Integer.MAX_VALUE),
523529
// Let us use the sane ordering as DnsNameResolver will be used when returning
@@ -530,7 +536,10 @@ private DnsServerAddressStream newQueryServerAddressStream(
530536
return new ThreadLocalNameServerAddressStream(dnsServerAddressStreamProvider);
531537
}
532538

533-
private DnsCnameCache newCnameCache() {
539+
DnsCnameCache getOrNewCnameCache() {
540+
if (this.cnameCache != null) {
541+
return this.cnameCache;
542+
}
534543
return new DefaultDnsCnameCache(
535544
intValue(minTtl, 0), intValue(maxTtl, Integer.MAX_VALUE));
536545
}
@@ -583,10 +592,9 @@ public DnsNameResolver build() {
583592
logger.debug("authoritativeDnsServerCache and TTLs are mutually exclusive. TTLs are ignored.");
584593
}
585594

586-
DnsCache resolveCache = this.resolveCache != null ? this.resolveCache : newCache();
587-
DnsCnameCache cnameCache = this.cnameCache != null ? this.cnameCache : newCnameCache();
588-
AuthoritativeDnsServerCache authoritativeDnsServerCache = this.authoritativeDnsServerCache != null ?
589-
this.authoritativeDnsServerCache : newAuthoritativeDnsServerCache();
595+
DnsCache resolveCache = getOrNewCache();
596+
DnsCnameCache cnameCache = getOrNewCnameCache();
597+
AuthoritativeDnsServerCache authoritativeDnsServerCache = getOrNewAuthoritativeDnsServerCache();
590598

591599
DnsServerAddressStream queryDnsServerAddressStream = this.queryDnsServerAddressStream != null ?
592600
this.queryDnsServerAddressStream : newQueryServerAddressStream(dnsServerAddressStreamProvider);

resolver-dns/src/test/java/io/netty/resolver/dns/DnsAddressResolverGroupTest.java

+51
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,21 @@
2020
import io.netty.channel.nio.NioEventLoopGroup;
2121
import io.netty.channel.socket.nio.NioDatagramChannel;
2222
import io.netty.resolver.AddressResolver;
23+
import io.netty.resolver.InetSocketAddressResolver;
2324
import io.netty.util.concurrent.Future;
2425
import io.netty.util.concurrent.FutureListener;
2526
import io.netty.util.concurrent.Promise;
2627
import org.junit.jupiter.api.Test;
2728

29+
import java.net.InetAddress;
30+
import java.net.InetSocketAddress;
2831
import java.net.SocketAddress;
2932
import java.nio.channels.UnsupportedAddressTypeException;
33+
import java.util.concurrent.ExecutionException;
3034

3135
import static org.hamcrest.MatcherAssert.assertThat;
3236
import static org.hamcrest.Matchers.instanceOf;
37+
import static org.junit.jupiter.api.Assertions.assertSame;
3338
import static org.junit.jupiter.api.Assertions.assertTrue;
3439

3540
public class DnsAddressResolverGroupTest {
@@ -66,4 +71,50 @@ public void operationComplete(Future<Object> future) {
6671
defaultEventLoopGroup.shutdownGracefully();
6772
}
6873
}
74+
75+
@Test
76+
public void testSharedDNSCacheAcrossEventLoops() throws InterruptedException, ExecutionException {
77+
NioEventLoopGroup group = new NioEventLoopGroup(1);
78+
final EventLoop loop = group.next();
79+
DnsNameResolverBuilder builder = new DnsNameResolverBuilder()
80+
.eventLoop(loop).channelType(NioDatagramChannel.class);
81+
DnsAddressResolverGroup resolverGroup = new DnsAddressResolverGroup(builder);
82+
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup(2);
83+
EventLoop eventLoop1 = defaultEventLoopGroup.next();
84+
EventLoop eventLoop2 = defaultEventLoopGroup.next();
85+
try {
86+
final Promise<InetSocketAddress> promise1 = loop.newPromise();
87+
InetSocketAddressResolver resolver1 = (InetSocketAddressResolver) resolverGroup.getResolver(eventLoop1);
88+
InetAddress address1 =
89+
resolve(resolver1, InetSocketAddress.createUnresolved("netty.io", 80), promise1);
90+
final Promise<InetSocketAddress> promise2 = loop.newPromise();
91+
InetSocketAddressResolver resolver2 = (InetSocketAddressResolver) resolverGroup.getResolver(eventLoop2);
92+
InetAddress address2 =
93+
resolve(resolver2, InetSocketAddress.createUnresolved("netty.io", 80), promise2);
94+
assertSame(address1, address2);
95+
} finally {
96+
resolverGroup.close();
97+
group.shutdownGracefully();
98+
defaultEventLoopGroup.shutdownGracefully();
99+
}
100+
}
101+
102+
private InetAddress resolve(InetSocketAddressResolver resolver, SocketAddress socketAddress,
103+
final Promise<InetSocketAddress> promise)
104+
throws InterruptedException, ExecutionException {
105+
resolver.resolve(socketAddress)
106+
.addListener(new FutureListener<InetSocketAddress>() {
107+
@Override
108+
public void operationComplete(Future<InetSocketAddress> future) {
109+
try {
110+
promise.setSuccess(future.get());
111+
} catch (Throwable cause) {
112+
promise.setFailure(cause);
113+
}
114+
}
115+
}).await();
116+
promise.sync();
117+
InetSocketAddress inetSocketAddress = promise.get();
118+
return inetSocketAddress.getAddress();
119+
}
69120
}

0 commit comments

Comments
 (0)