/*
* Copyright (C) 2015 SoftIndex LLC.
*
* Licensed 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
*
* http://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.datakernel.dns;
import io.datakernel.async.ResultCallback;
import io.datakernel.time.CurrentTimeProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetAddress;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import static io.datakernel.dns.DnsMessage.AAAA_RECORD_TYPE;
import static io.datakernel.dns.DnsMessage.A_RECORD_TYPE;
/**
* Represents a cache for storing resolved domains during its time to live.
*/
final class DnsCache {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final Map<String, CachedDnsLookupResult> cache = new ConcurrentHashMap<>();
private final Map<Long, Set<String>> expirations = new HashMap<>();
private long lastCleanupSecond;
private final long errorCacheExpirationSeconds;
private final long hardExpirationDeltaSeconds;
private final CurrentTimeProvider timeProvider;
private long maxTtlSeconds = Long.MAX_VALUE;
/**
* Enum with freshness cache's entry.
* <ul>
* <li>FRESH - while time to live of this entry has not passed, empty is considered resolved
* <li> SOFT_TTL_EXPIRED - while hard time expiration has not passed, empty is considered resolved, but needs refreshing
* <li> HARD_TTL_EXPIRED - while hard time expiration has passed, empty is considered not resolved
* </ul>
*/
public enum DnsCacheEntryFreshness {
FRESH,
SOFT_TTL_EXPIRED,
HARD_TTL_EXPIRED,
}
public enum DnsCacheQueryResult {
RESOLVED,
RESOLVED_NEEDS_REFRESHING,
NOT_RESOLVED
}
/**
* Creates a new DNS cache.
*
* @param timeProvider time provider
* @param errorCacheExpirationMillis expiration time for errors without time to live
* @param hardExpirationDeltaMillis delta between time at which entry is considered resolved, but needs
* refreshing and time at which entry is considered not resolved
*/
private DnsCache(CurrentTimeProvider timeProvider, long errorCacheExpirationMillis, long hardExpirationDeltaMillis) {
this.errorCacheExpirationSeconds = errorCacheExpirationMillis / 1000;
this.hardExpirationDeltaSeconds = hardExpirationDeltaMillis / 1000;
this.timeProvider = timeProvider;
this.lastCleanupSecond = getCurrentSecond();
}
public static DnsCache create(CurrentTimeProvider timeProvider, long errorCacheExpirationMillis,
long hardExpirationDeltaMillis) {
return new DnsCache(timeProvider, errorCacheExpirationMillis, hardExpirationDeltaMillis);
}
private boolean isRequestedType(CachedDnsLookupResult cachedResult, boolean requestedIpv6) {
Short cachedResultType = cachedResult.getType();
if (cachedResultType == A_RECORD_TYPE & !requestedIpv6)
return true;
if (cachedResultType == AAAA_RECORD_TYPE & requestedIpv6)
return true;
else return false;
}
/**
* Tries to get status of the entry for some domain name from the cache.
*
* @param domainName domain name for finding entry
* @param ipv6 type of result, if true - IPv6, false - IPv4
* @param callback callback with which it will handle result
* @return DnsCacheQueryResult for this domain name
*/
public DnsCacheQueryResult tryToResolve(String domainName, boolean ipv6, ResultCallback<InetAddress[]> callback) {
CachedDnsLookupResult cachedResult = cache.get(domainName);
if (cachedResult == null) {
if (logger.isDebugEnabled())
logger.debug("Cache miss for host: {}", domainName);
return DnsCacheQueryResult.NOT_RESOLVED;
}
if (cachedResult.isSuccessful() && !isRequestedType(cachedResult, ipv6)) {
if (logger.isDebugEnabled())
logger.debug("Cache miss for host: {}", domainName);
return DnsCacheQueryResult.NOT_RESOLVED;
}
DnsCacheEntryFreshness freshness = getResultFreshness(cachedResult);
switch (freshness) {
case HARD_TTL_EXPIRED: {
if (logger.isDebugEnabled())
logger.debug("Hard TTL expired for host: {}", domainName);
return DnsCacheQueryResult.NOT_RESOLVED;
}
case SOFT_TTL_EXPIRED: {
if (logger.isDebugEnabled())
logger.debug("Soft TTL expired for host: {}", domainName);
returnResultThroughCallback(domainName, cachedResult, callback);
return DnsCacheQueryResult.RESOLVED_NEEDS_REFRESHING;
}
default: {
returnResultThroughCallback(domainName, cachedResult, callback);
return DnsCacheQueryResult.RESOLVED;
}
}
}
private void returnResultThroughCallback(String domainName, CachedDnsLookupResult result, ResultCallback<InetAddress[]> callback) {
if (result.isSuccessful()) {
InetAddress[] ipsFromCache = result.getIps();
callback.setResult(ipsFromCache);
if (logger.isDebugEnabled())
logger.debug("Cache hit for host: {}", domainName);
} else {
DnsException exception = result.getException();
callback.setException(exception);
if (logger.isDebugEnabled())
logger.debug("Error cache hit for host: {}", domainName);
}
}
private DnsCacheEntryFreshness getResultFreshness(CachedDnsLookupResult result) {
long softExpirationSecond = result.getExpirationSecond();
long hardExpirationSecond = getHardExpirationSecond(softExpirationSecond);
long currentSecond = getCurrentSecond();
if (currentSecond >= hardExpirationSecond)
return DnsCacheEntryFreshness.HARD_TTL_EXPIRED;
else if (currentSecond >= softExpirationSecond)
return DnsCacheEntryFreshness.SOFT_TTL_EXPIRED;
else
return DnsCacheEntryFreshness.FRESH;
}
/**
* Adds DnsQueryResult to this cache
*
* @param result result to add
*/
public void add(DnsQueryResult result) {
if (result.getMinTtl() == 0)
return;
long expirationSecond;
if (result.getMinTtl() > maxTtlSeconds)
expirationSecond = maxTtlSeconds + getCurrentSecond();
else
expirationSecond = result.getMinTtl() + getCurrentSecond();
String domainName = result.getDomainName();
cache.put(domainName, CachedDnsLookupResult.fromQueryWithExpiration(result, expirationSecond));
setExpiration(expirations, expirationSecond + hardExpirationDeltaSeconds, domainName);
if (logger.isDebugEnabled())
logger.debug("Add result to cache for host: {}", domainName);
}
/**
* Adds DnsException to this cache
*
* @param exception exception to add
*/
public void add(DnsException exception) {
long expirationSecond = errorCacheExpirationSeconds + getCurrentSecond();
String domainName = exception.getDomainName();
cache.put(domainName, CachedDnsLookupResult.fromExceptionWithExpiration(exception, expirationSecond));
setExpiration(expirations, expirationSecond + hardExpirationDeltaSeconds, domainName);
if (logger.isDebugEnabled())
logger.debug("Add exception to cache for host: {}", domainName);
}
public void performCleanup() {
long callSecond = getCurrentSecond();
if (callSecond > lastCleanupSecond) {
clear(callSecond, lastCleanupSecond);
lastCleanupSecond = callSecond;
}
}
private void clear(long callSecond, long lastCleanupSecond) {
for (long i = lastCleanupSecond; i <= callSecond; ++i) {
Collection<String> domainNames = expirations.remove(i);
if (domainNames != null) {
for (String domainName : domainNames) {
CachedDnsLookupResult cachedResult = cache.get(domainName);
if (cachedResult != null && getResultFreshness(cachedResult) == DnsCacheEntryFreshness.HARD_TTL_EXPIRED) {
cache.remove(domainName);
}
}
}
}
}
public long getMaxTtlSeconds() {
return maxTtlSeconds;
}
public void setMaxTtlSeconds(long maxTtlSeconds) {
this.maxTtlSeconds = maxTtlSeconds;
}
public void clear() {
cache.clear();
expirations.clear();
}
private long getCurrentSecond() {
return timeProvider.currentTimeMillis() / 1000;
}
private long getHardExpirationSecond(long softExpirationSecond) {
return softExpirationSecond + hardExpirationDeltaSeconds;
}
public int getNumberOfCachedDomainNames() {
return cache.size();
}
public int getNumberOfCachedExceptions() {
int exceptions = 0;
for (CachedDnsLookupResult cachedResult : cache.values()) {
if (!cachedResult.isSuccessful())
++exceptions;
}
return exceptions;
}
public String[] getSuccessfullyResolvedDomainNames() {
List<String> domainNames = new ArrayList<>();
for (Map.Entry<String, CachedDnsLookupResult> entry : cache.entrySet()) {
if (entry.getValue().isSuccessful()) {
domainNames.add(entry.getKey());
}
}
return domainNames.toArray(new String[domainNames.size()]);
}
public String[] getDomainNamesOfFailedRequests() {
List<String> domainNames = new ArrayList<>();
for (Map.Entry<String, CachedDnsLookupResult> entry : cache.entrySet()) {
if (!entry.getValue().isSuccessful()) {
domainNames.add(entry.getKey());
}
}
return domainNames.toArray(new String[domainNames.size()]);
}
public String[] getAllCacheEntries() {
List<String> cacheEntries = new ArrayList<>();
StringBuilder sb = new StringBuilder();
if (!cache.isEmpty())
cacheEntries.add("domainName;ips;secondsToSoftExpiration;secondsToHardExpiration");
for (Map.Entry<String, CachedDnsLookupResult> detailedCacheEntry : cache.entrySet()) {
String domainName = detailedCacheEntry.getKey();
InetAddress[] ips = detailedCacheEntry.getValue().getIps();
long softExpirationSecond = detailedCacheEntry.getValue().getExpirationSecond();
long hardExpirationSecond = getHardExpirationSecond(softExpirationSecond);
long currentSecond = getCurrentSecond();
long secondsToSoftExpiration = softExpirationSecond - currentSecond;
long secondsToHardExpiration = hardExpirationSecond - currentSecond;
sb.append(domainName);
sb.append(";");
sb.append(Arrays.toString(ips));
sb.append(";");
sb.append(secondsToSoftExpiration <= 0 ? "expired" : secondsToSoftExpiration);
sb.append(";");
sb.append(secondsToHardExpiration <= 0 ? "expired" : secondsToHardExpiration);
cacheEntries.add(sb.toString());
sb.setLength(0);
}
return cacheEntries.toArray(new String[cacheEntries.size()]);
}
private void setExpiration(Map<Long, Set<String>> expirations, long time, String domain) {
Set<String> sameTime = expirations.get(time);
if (sameTime == null) {
sameTime = new HashSet<>();
expirations.put(time, sameTime);
}
sameTime.add(domain);
}
}