Skip to content

Commit

Permalink
The Vert.x DNS address resolver can load an hosts configuration file …
Browse files Browse the repository at this point in the history
…and cache the content forever. Sometimes the content of the file can change and the cached content becomes stale.

Add a new AddressResolverOptions hostsRefreshPeriod property to let the resolver refresh the hosts file when its cached content is older than the last read + refresh period.
  • Loading branch information
vietj committed Sep 8, 2023
1 parent c15ee60 commit 0e3608c
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 80 deletions.
32 changes: 32 additions & 0 deletions src/main/java/io/vertx/core/dns/AddressResolverOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ public class AddressResolverOptions {
*/
public static final int DEFAULT_QUERY_TIMEOUT = 5000;

/**
* The default value for the hosts refresh value in millis = 0 (disabled)
*/
public static final int DEFAULT_HOSTS_REFRESH_PERIOD = 0;

/**
* The default value for the max dns queries per query = 4
*/
Expand Down Expand Up @@ -90,6 +95,7 @@ public class AddressResolverOptions {

private String hostsPath;
private Buffer hostsValue;
private int hostsRefreshPeriod;
private List<String> servers;
private boolean optResourceEnabled;
private int cacheMinTimeToLive;
Expand All @@ -116,11 +122,13 @@ public AddressResolverOptions() {
ndots = DEFAULT_NDOTS;
rotateServers = DEFAULT_ROTATE_SERVERS;
roundRobinInetAddress = DEFAULT_ROUND_ROBIN_INET_ADDRESS;
hostsRefreshPeriod = DEFAULT_HOSTS_REFRESH_PERIOD;
}

public AddressResolverOptions(AddressResolverOptions other) {
this.hostsPath = other.hostsPath;
this.hostsValue = other.hostsValue != null ? other.hostsValue.copy() : null;
this.hostsRefreshPeriod = other.hostsRefreshPeriod;
this.servers = other.servers != null ? new ArrayList<>(other.servers) : null;
this.optResourceEnabled = other.optResourceEnabled;
this.cacheMinTimeToLive = other.cacheMinTimeToLive;
Expand Down Expand Up @@ -182,6 +190,30 @@ public AddressResolverOptions setHostsValue(Buffer hostsValue) {
return this;
}

/**
* @return the hosts configuration refresh period in millis
*/
public int getHostsRefreshPeriod() {
return hostsRefreshPeriod;
}

/**
* Set the hosts configuration refresh period in millis, {@code 0} disables it.
* <p/>
* The resolver caches the hosts configuration {@link #hostsPath file} after it has read it. When
* the content of this file can change, setting a positive refresh period will load the configuration
* file again when necessary.
*
* @param hostsRefreshPeriod the hosts configuration refresh period
*/
public AddressResolverOptions setHostsRefreshPeriod(int hostsRefreshPeriod) {
if (hostsRefreshPeriod < 0) {
throw new IllegalArgumentException("hostsRefreshPeriod must be >= 0");
}
this.hostsRefreshPeriod = hostsRefreshPeriod;
return this;
}

/**
* @return the list of dns server
*/
Expand Down
1 change: 1 addition & 0 deletions src/main/java/io/vertx/core/impl/AddressResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.vertx.core.impl.launcher.commands.ExecUtils;
import io.vertx.core.impl.logging.Logger;
import io.vertx.core.impl.logging.LoggerFactory;
import io.vertx.core.impl.resolver.DnsResolverProvider;
import io.vertx.core.spi.resolver.ResolverProvider;

import java.io.File;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/vertx/core/impl/VertxImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ public DnsClient createDnsClient(DnsClientOptions options) {
String host = options.getHost();
int port = options.getPort();
if (host == null || port < 0) {
DnsResolverProvider provider = new DnsResolverProvider(this, addressResolverOptions);
DnsResolverProvider provider = DnsResolverProvider.create(this, addressResolverOptions);
InetSocketAddress address = provider.nameServerAddresses().get(0);
// provide the host and port
options = new DnsClientOptions(options)
Expand Down
183 changes: 105 additions & 78 deletions src/main/java/io/vertx/core/impl/resolver/DnsResolverProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,17 @@

package io.vertx.core.impl.resolver;

import io.netty.channel.ChannelFactory;
import io.netty.channel.EventLoop;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.resolver.*;
import io.netty.resolver.dns.*;
import io.netty.util.NetUtil;
import io.netty.util.concurrent.EventExecutor;
import io.vertx.core.Context;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.VertxException;
import io.vertx.core.*;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.impl.VertxInternal;
import io.vertx.core.dns.AddressResolverOptions;
import io.vertx.core.impl.AddressResolver;
import io.vertx.core.impl.VertxImpl;
import io.vertx.core.impl.ContextInternal;
import io.vertx.core.spi.resolver.ResolverProvider;

import java.io.File;
Expand All @@ -34,27 +30,32 @@
import java.net.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import static io.netty.util.internal.ObjectUtil.intValue;

/**
* @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
*/
public class DnsResolverProvider implements ResolverProvider {
public class DnsResolverProvider implements ResolverProvider, HostsFileEntriesResolver {

private final Vertx vertx;
public static DnsResolverProvider create(VertxInternal vertx, AddressResolverOptions options) {
DnsResolverProvider provider = new DnsResolverProvider(vertx, options);
provider.refresh();
return provider;
}

private final VertxInternal vertx;
private final List<ResolverRegistration> resolvers = Collections.synchronizedList(new ArrayList<>());
private AddressResolverGroup<InetSocketAddress> resolverGroup;
private final List<InetSocketAddress> serverList = new ArrayList<>();
private final String hostsPath;
private final Buffer hostsValue;
private final AtomicLong refreshTimestamp = new AtomicLong();
private final long hostsRefreshPeriodNanos;
private volatile HostsFileEntries parsedHostsFile = new HostsFileEntries(Collections.emptyMap(), Collections.emptyMap());

/**
* @return a list of DNS servers available to use
*/
public List<InetSocketAddress> nameServerAddresses() {
return serverList;
}

public DnsResolverProvider(VertxImpl vertx, AddressResolverOptions options) {
private DnsResolverProvider(VertxInternal vertx, AddressResolverOptions options) {
List<String> dnsServers = options.getServers();
if (dnsServers != null && dnsServers.size() > 0) {
for (String dnsServer : dnsServers) {
Expand Down Expand Up @@ -87,30 +88,8 @@ public DnsResolverProvider(VertxImpl vertx, AddressResolverOptions options) {
}
}
DnsServerAddresses nameServerAddresses = options.isRotateServers() ? DnsServerAddresses.rotational(serverList) : DnsServerAddresses.sequential(serverList);
DnsServerAddressStreamProvider nameServerAddressProvider = hostname -> {
return nameServerAddresses.stream();
};
DnsServerAddressStreamProvider nameServerAddressProvider = hostname -> nameServerAddresses.stream();

HostsFileEntries entries;
if (options.getHostsPath() != null) {
File file = vertx.resolveFile(options.getHostsPath()).getAbsoluteFile();
try {
if (!file.exists() || !file.isFile()) {
throw new IOException();
}
entries = HostsFileParser.parse(file);
} catch (IOException e) {
throw new VertxException("Cannot read hosts file " + file.getAbsolutePath());
}
} else if (options.getHostsValue() != null) {
try {
entries = HostsFileParser.parse(new StringReader(options.getHostsValue().toString()));
} catch (IOException e) {
throw new VertxException("Cannot read hosts config ", e);
}
} else {
entries = HostsFileParser.parseSilently();
}

int minTtl = intValue(options.getCacheMinTimeToLive(), 0);
int maxTtl = intValue(options.getCacheMaxTimeToLive(), Integer.MAX_VALUE);
Expand All @@ -119,38 +98,12 @@ public DnsResolverProvider(VertxImpl vertx, AddressResolverOptions options) {
DnsCache authoritativeDnsServerCache = new DefaultDnsCache(minTtl, maxTtl, negativeTtl);

this.vertx = vertx;

this.hostsPath = options.getHostsPath();
this.hostsValue = options.getHostsValue();
this.hostsRefreshPeriodNanos = options.getHostsRefreshPeriod();

DnsNameResolverBuilder builder = new DnsNameResolverBuilder();
builder.hostsFileEntriesResolver(new HostsFileEntriesResolver() {
@Override
public InetAddress address(String inetHost, ResolvedAddressTypes resolvedAddressTypes) {
if (inetHost.endsWith(".")) {
inetHost = inetHost.substring(0, inetHost.length() - 1);
}
InetAddress address = lookup(inetHost, resolvedAddressTypes);
if (address == null) {
address = lookup(inetHost.toLowerCase(Locale.ENGLISH), resolvedAddressTypes);
}
return address;
}
InetAddress lookup(String inetHost, ResolvedAddressTypes resolvedAddressTypes) {
switch (resolvedAddressTypes) {
case IPV4_ONLY:
return entries.inet4Entries().get(inetHost);
case IPV6_ONLY:
return entries.inet6Entries().get(inetHost);
case IPV4_PREFERRED:
Inet4Address inet4Address = entries.inet4Entries().get(inetHost);
return inet4Address != null? inet4Address : entries.inet6Entries().get(inetHost);
case IPV6_PREFERRED:
Inet6Address inet6Address = entries.inet6Entries().get(inetHost);
return inet6Address != null? inet6Address : entries.inet4Entries().get(inetHost);
default:
throw new IllegalArgumentException("Unknown ResolvedAddressTypes " + resolvedAddressTypes);
}
}
});
builder.hostsFileEntriesResolver(this);
builder.channelFactory(() -> vertx.transport().datagramChannel());
builder.socketChannelFactory(() -> (SocketChannel) vertx.transport().channelFactory(false).newChannel());
builder.nameServerProvider(nameServerAddressProvider);
Expand Down Expand Up @@ -184,23 +137,52 @@ protected io.netty.resolver.AddressResolver<InetSocketAddress> newAddressResolve
};
}

private static class ResolverRegistration {
private final io.netty.resolver.AddressResolver<InetSocketAddress> resolver;
private final EventLoop executor;
ResolverRegistration(io.netty.resolver.AddressResolver<InetSocketAddress> resolver, EventLoop executor) {
this.resolver = resolver;
this.executor = executor;
@Override
public InetAddress address(String inetHost, ResolvedAddressTypes resolvedAddressTypes) {
if (inetHost.endsWith(".")) {
inetHost = inetHost.substring(0, inetHost.length() - 1);
}
if (hostsRefreshPeriodNanos > 0) {
ensureHostsFileFresh(hostsRefreshPeriodNanos);
}
InetAddress address = lookup(inetHost, resolvedAddressTypes);
if (address == null) {
address = lookup(inetHost.toLowerCase(Locale.ENGLISH), resolvedAddressTypes);
}
return address;
}
InetAddress lookup(String inetHost, ResolvedAddressTypes resolvedAddressTypes) {
switch (resolvedAddressTypes) {
case IPV4_ONLY:
return parsedHostsFile.inet4Entries().get(inetHost);
case IPV6_ONLY:
return parsedHostsFile.inet6Entries().get(inetHost);
case IPV4_PREFERRED:
Inet4Address inet4Address = parsedHostsFile.inet4Entries().get(inetHost);
return inet4Address != null? inet4Address : parsedHostsFile.inet6Entries().get(inetHost);
case IPV6_PREFERRED:
Inet6Address inet6Address = parsedHostsFile.inet6Entries().get(inetHost);
return inet6Address != null? inet6Address : parsedHostsFile.inet4Entries().get(inetHost);
default:
throw new IllegalArgumentException("Unknown ResolvedAddressTypes " + resolvedAddressTypes);
}
}

/**
* @return a list of DNS servers available to use
*/
public List<InetSocketAddress> nameServerAddresses() {
return serverList;
}

@Override
public AddressResolverGroup<InetSocketAddress> resolver(AddressResolverOptions options) {
return resolverGroup;
}

@Override
public void close(Handler<Void> doneHandler) {
Context context = vertx.getOrCreateContext();
ContextInternal context = vertx.getOrCreateContext();
ResolverRegistration[] registrations = this.resolvers.toArray(new ResolverRegistration[0]);
if (registrations.length == 0) {
context.runOnContext(doneHandler);
Expand All @@ -221,4 +203,49 @@ public void close(Handler<Void> doneHandler) {
}
}
}

public void refresh() {
ensureHostsFileFresh(0);
}

private void ensureHostsFileFresh(long refreshPeriodNanos) {
long prev = refreshTimestamp.get();
long now = System.nanoTime();
if ((now - prev) >= refreshPeriodNanos && refreshTimestamp.compareAndSet(prev, now)) {
refreshHostsFile();
}
}

private void refreshHostsFile() {
HostsFileEntries entries;
if (hostsPath != null) {
File file = vertx.resolveFile(hostsPath).getAbsoluteFile();
try {
if (!file.exists() || !file.isFile()) {
throw new IOException();
}
entries = HostsFileParser.parse(file);
} catch (IOException e) {
throw new VertxException("Cannot read hosts file " + file.getAbsolutePath());
}
} else if (hostsValue != null) {
try {
entries = HostsFileParser.parse(new StringReader(hostsValue.toString()));
} catch (IOException e) {
throw new VertxException("Cannot read hosts config ", e);
}
} else {
entries = HostsFileParser.parseSilently();
}
parsedHostsFile = entries;
}

private static class ResolverRegistration {
private final io.netty.resolver.AddressResolver<InetSocketAddress> resolver;
private final EventLoop executor;
ResolverRegistration(io.netty.resolver.AddressResolver<InetSocketAddress> resolver, EventLoop executor) {
this.resolver = resolver;
this.executor = executor;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import io.vertx.core.VertxException;
import io.vertx.core.dns.AddressResolverOptions;
import io.vertx.core.impl.VertxImpl;
import io.vertx.core.impl.VertxInternal;
import io.vertx.core.impl.resolver.DnsResolverProvider;
import io.vertx.core.impl.resolver.DefaultResolverProvider;
import io.vertx.core.impl.logging.Logger;
Expand All @@ -36,7 +37,7 @@ static ResolverProvider factory(Vertx vertx, AddressResolverOptions options) {
// that use an unstable API and fallback on the default (blocking) provider
try {
if (!Boolean.getBoolean(DISABLE_DNS_RESOLVER_PROP_NAME)) {
return new DnsResolverProvider((VertxImpl) vertx, options);
return DnsResolverProvider.create((VertxInternal) vertx, options);
}
} catch (Throwable e) {
if (e instanceof VertxException) {
Expand Down
31 changes: 31 additions & 0 deletions src/test/java/io/vertx/core/dns/HostnameResolutionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
Expand Down Expand Up @@ -415,6 +416,36 @@ public void testTrailingDotResolveFromHosts() {
await();
}

@Test
public void testRefreshHosts1() throws Exception {
InetAddress addr = testRefreshHosts((int) TimeUnit.SECONDS.toNanos(1));
assertEquals("192.168.0.16", addr.getHostAddress());
assertEquals("server.net", addr.getHostName());
}

@Test
public void testRefreshHosts2() throws Exception {
InetAddress addr = testRefreshHosts(0);
assertEquals("192.168.0.15", addr.getHostAddress());
assertEquals("server.net", addr.getHostName());
}

private InetAddress testRefreshHosts(int period) throws Exception {
File hosts = File.createTempFile("vertx", "hosts");
hosts.deleteOnExit();
Files.writeString(hosts.toPath(), "192.168.0.15 server.net");
AddressResolverOptions options = new AddressResolverOptions()
.setHostsPath(hosts.getAbsolutePath())
.setHostsRefreshPeriod(period);
VertxInternal vertx = (VertxInternal) vertx(new VertxOptions().setAddressResolverOptions(options));
InetAddress addr = awaitFuture(vertx.resolveAddress("server.net"));
assertEquals("192.168.0.15", addr.getHostAddress());
assertEquals("server.net", addr.getHostName());
Files.writeString(hosts.toPath(), "192.168.0.16 server.net");
Thread.sleep(1000);
return awaitFuture(vertx.resolveAddress("server.net"));
}

@Test
public void testResolveMissingLocalhost() throws Exception {

Expand Down

0 comments on commit 0e3608c

Please sign in to comment.