Skip to content

Commit

Permalink
Adds a new public method to allow the cache to be queried. The point …
Browse files Browse the repository at this point in the history
…of this is to allow an application to take different actions depending on whether a network lookup needs to be undertaken or not. See MichaCo#80.

Adds a setting to choose whether lookup failures should be cached and if so for how long. The point of this is to allow applciations to avoid high frequency lookups on failing queries.
  • Loading branch information
sipsorcery committed Jul 30, 2020
1 parent 21ca526 commit 0933a93
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 50 deletions.
4 changes: 2 additions & 2 deletions src/DnsClient/DnsQueryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -618,14 +618,14 @@ public static async Task<ServiceHostEntry[]> ResolveServiceAsync(this IDnsQuery
return ResolveServiceProcessResult(result);
}

private static string ConcatResolveServiceName(string baseDomain, string serviceName, string tag)
public static string ConcatResolveServiceName(string baseDomain, string serviceName, string tag)
{
return string.IsNullOrWhiteSpace(tag) ?
$"{serviceName}.{baseDomain}." :
$"_{serviceName}._{tag}.{baseDomain}.";
}

private static ServiceHostEntry[] ResolveServiceProcessResult(IDnsQueryResponse result)
public static ServiceHostEntry[] ResolveServiceProcessResult(IDnsQueryResponse result)
{
var hosts = new List<ServiceHostEntry>();
if (result == null || result.HasError)
Expand Down
34 changes: 32 additions & 2 deletions src/DnsClient/DnsQueryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,20 @@ public int ExtendedDnsBufferSize
/// Gets or sets a flag indicating whether EDNS should be enabled and the <c>DO</c> flag should be set.
/// Defaults to <c>False</c>.
/// </summary>
public bool RequestDnsSecRecords { get; set; } = false;
public bool RequestDnsSecRecords { get; set; } = false;

/// <summary>
/// Gets or sets a flag indicating whether the DNS failures are being cached. The purpose of caching
/// failures is to reduce repeated lookup attempts within a short space of time.
/// Defaults to <c>False</c>.
/// </summary>
public bool UseCacheForFailures { get; set; } = false;

/// <summary>
/// Gets or sets the duration to cache failed lookups. Does not apply if failed lookups are not being cached.
/// Defaults to <c>5 seconds</c>.
/// </summary>
public TimeSpan CacheFailureDuration { get; set; } = s_defaultTimeout;

/// <summary>
/// Converts the query options into readonly settings.
Expand Down Expand Up @@ -614,6 +627,19 @@ public class DnsQuerySettings : IEquatable<DnsQuerySettings>
/// </summary>
public bool RequestDnsSecRecords { get; }

/// <summary>
/// Gets a flag indicating whether the DNS failures are being cached. The purpose of caching
/// failures is to reduce repeated lookup attempts within a short space of time.
/// Defaults to <c>False</c>.
/// </summary>
public bool UseCacheForFailures { get; }

/// <summary>
/// If failures are being cached this value indicates how long they will be held in the cache for.
/// Defaults to <c>5 seconds</c>.
/// </summary>
public TimeSpan CacheFailureDuration { get; }

/// <summary>
/// Creates a new instance of <see cref="DnsQueryAndServerSettings"/>.
/// </summary>
Expand All @@ -637,6 +663,8 @@ public DnsQuerySettings(DnsQueryOptions options)
UseTcpOnly = options.UseTcpOnly;
ExtendedDnsBufferSize = options.ExtendedDnsBufferSize;
RequestDnsSecRecords = options.RequestDnsSecRecords;
UseCacheForFailures = options.UseCacheForFailures;
CacheFailureDuration = options.CacheFailureDuration;
}

/// <inheritdocs />
Expand Down Expand Up @@ -680,7 +708,9 @@ public bool Equals(DnsQuerySettings other)
UseTcpFallback == other.UseTcpFallback &&
UseTcpOnly == other.UseTcpOnly &&
ExtendedDnsBufferSize == other.ExtendedDnsBufferSize &&
RequestDnsSecRecords == other.RequestDnsSecRecords;
RequestDnsSecRecords == other.RequestDnsSecRecords &&
UseCacheForFailures == other.UseCacheForFailures &&
CacheFailureDuration.Equals(other.Timeout);
}
}

Expand Down
76 changes: 63 additions & 13 deletions src/DnsClient/LookupClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -376,12 +376,12 @@ internal LookupClient(LookupClientOptions options, DnsMessageHandler udpHandler
if (options.AutoResolveNameServers)
{
_resolvedNameServers = NameServer.ResolveNameServers(skipIPv6SiteLocal: true, fallbackToGooglePublicDns: false);
servers = servers.Concat(_resolvedNameServers).ToArray();
// This will periodically get triggered on Query calls and
// will perform the same check as on NetworkAddressChanged.
// The event doesn't seem to get fired on Linux for example...
// TODO: Maybe there is a better way, but this will work for now.
servers = servers.Concat(_resolvedNameServers).ToArray();

// This will periodically get triggered on Query calls and
// will perform the same check as on NetworkAddressChanged.
// The event doesn't seem to get fired on Linux for example...
// TODO: Maybe there is a better way, but this will work for now.
_skipper = new SkipWorker(
() =>
{
Expand All @@ -396,7 +396,7 @@ internal LookupClient(LookupClientOptions options, DnsMessageHandler udpHandler
}

Settings = new LookupClientSettings(options, servers);
Cache = new ResponseCache(true, Settings.MinimumCacheTimeout, Settings.MaximumCacheTimeout);
Cache = new ResponseCache(true, Settings.MinimumCacheTimeout, Settings.MaximumCacheTimeout, Settings.CacheFailureDuration);
}

private void CheckResolvedNameservers()
Expand Down Expand Up @@ -482,6 +482,22 @@ public IDnsQueryResponse Query(DnsQuestion question, DnsQueryAndServerOptions qu

var settings = GetSettings(queryOptions);
return QueryInternal(question, settings, settings.ShuffleNameServers());
}

/// <inheritdoc/>
public IDnsQueryResponse QueryCache(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN)
=> QueryCache(new DnsQuestion(query, queryType, queryClass));

/// <inheritdoc/>
public IDnsQueryResponse QueryCache(DnsQuestion question)
{
if (question is null)
{
throw new ArgumentNullException(nameof(question));
}

var settings = GetSettings();
return QueryCache(question, settings);
}

/// <inheritdoc/>
Expand Down Expand Up @@ -875,9 +891,14 @@ private async Task<IDnsQueryResponse> QueryInternalAsync(DnsQuestion question, D
if (lastQueryResponse == null)
{
throw;
}

// If its the last server, return.
if (settings.UseCache && settings.UseCacheForFailures)
{
Cache.Add(cacheKey, lastQueryResponse, true);
}

// If its the last server, return.
return lastQueryResponse;
}
catch (Exception ex) when (
Expand Down Expand Up @@ -1126,9 +1147,14 @@ private async Task<IDnsQueryResponse> QueryInternalAsync(DnsQuestion question, D
if (lastQueryResponse == null)
{
throw;
}

// If its the last server, return.
if (settings.UseCache && settings.UseCacheForFailures)
{
Cache.Add(cacheKey, lastQueryResponse, true);
}

// If its the last server, return.
return lastQueryResponse;
}
catch (Exception ex) when (
Expand Down Expand Up @@ -1200,6 +1226,30 @@ private async Task<IDnsQueryResponse> QueryInternalAsync(DnsQuestion question, D
{
AuditTrail = audit?.Build()
};
}

private IDnsQueryResponse QueryCache(
DnsQuestion question,
DnsQuerySettings settings)
{
if (question == null)
{
throw new ArgumentNullException(nameof(question));
}

var head = new DnsRequestHeader(false, DnsOpCode.Query);
var request = new DnsRequestMessage(head, question);

var cacheKey = ResponseCache.GetCacheKey(request.Question);

if (TryGetCachedResult(cacheKey, request, settings, out var cachedResponse))
{
return cachedResponse;
}
else
{
return null;
}
}

private enum HandleError
Expand Down Expand Up @@ -1269,10 +1319,10 @@ private HandleError HandleDnsResponseException(DnsResponseException ex, DnsReque
private HandleError HandleDnsResponeParseException(DnsResponseParseException ex, DnsRequestMessage request, DnsMessageHandleType handleType, bool isLastServer)
{
// Don't try to fallback to TCP if we already are on TCP
if (handleType == DnsMessageHandleType.UDP
// Assuming that if we only got 512 or less bytes, its probably some network issue.
&& (ex.ResponseData.Length <= DnsQueryOptions.MinimumBufferSize
// Second assumption: If the parser tried to read outside the provided data, this might also be a network issue.
if (handleType == DnsMessageHandleType.UDP
// Assuming that if we only got 512 or less bytes, its probably some network issue.
&& (ex.ResponseData.Length <= DnsQueryOptions.MinimumBufferSize
// Second assumption: If the parser tried to read outside the provided data, this might also be a network issue.
|| ex.ReadLength + ex.Index > ex.ResponseData.Length))
{
// lets assume the response was truncated and retry with TCP.
Expand Down
94 changes: 61 additions & 33 deletions src/DnsClient/ResponseCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal class ResponseCache
private int _lastCleanup = 0;
private TimeSpan? _minimumTimeout;
private TimeSpan? _maximumTimeout;
private TimeSpan? _failureEntryTimeout;

public int Count => _cache.Count;

Expand Down Expand Up @@ -54,13 +55,29 @@ internal class ResponseCache

_maximumTimeout = value;
}
}

public TimeSpan? FailureEntryTimeout
{
get { return _failureEntryTimeout; }
set
{
if (value.HasValue &&
(value < TimeSpan.Zero || value > s_maxTimeout) && value != s_infiniteTimeout)
{
throw new ArgumentOutOfRangeException(nameof(value));
}

_failureEntryTimeout = value;
}
}

public ResponseCache(bool enabled = true, TimeSpan? minimumTimout = null, TimeSpan? maximumTimeout = null)
public ResponseCache(bool enabled = true, TimeSpan? minimumTimout = null, TimeSpan? maximumTimeout = null, TimeSpan? failureEntryTimeout = null)
{
Enabled = enabled;
MinimumTimout = minimumTimout;
MaximumTimeout = maximumTimeout;
FailureEntryTimeout = failureEntryTimeout;
}

public static string GetCacheKey(DnsQuestion question)
Expand Down Expand Up @@ -101,43 +118,54 @@ public IDnsQueryResponse Get(string key, out double? effectiveTtl)
return null;
}

public bool Add(string key, IDnsQueryResponse response)
public bool Add(string key, IDnsQueryResponse response, bool cacheFailures = false)
{
if (key == null) throw new ArgumentNullException(key);

if (Enabled && response != null && !response.HasError && response.Answers.Count > 0)
if (Enabled && response != null && (cacheFailures || (!response.HasError && response.Answers.Count > 0)))
{
var all = response.AllRecords.Where(p => !(p is Protocol.Options.OptRecord));
if (all.Any())
{
// in millis
double minTtl = all.Min(p => p.InitialTimeToLive) * 1000d;

if (MinimumTimout == Timeout.InfiniteTimeSpan)
{
// TODO: Log warning once?
minTtl = s_maxTimeout.TotalMilliseconds;
}
else if (MinimumTimout.HasValue && minTtl < MinimumTimout.Value.TotalMilliseconds)
{
minTtl = MinimumTimout.Value.TotalMilliseconds;
}

// max TTL check which can limit the upper boundary
if (MaximumTimeout.HasValue && MaximumTimeout != Timeout.InfiniteTimeSpan && minTtl > MaximumTimeout.Value.TotalMilliseconds)
{
minTtl = MaximumTimeout.Value.TotalMilliseconds;
}

if (minTtl < 1d)
{
return false;
if (response.Answers.Count == 0 && FailureEntryTimeout.HasValue)
{
// Cache entry for a failure response.
var newEntry = new ResponseEntry(response, FailureEntryTimeout.Value.TotalMilliseconds);

StartCleanup();
return _cache.TryAdd(key, newEntry);
}
else
{
var all = response.AllRecords.Where(p => !(p is Protocol.Options.OptRecord));
if (all.Any())
{
// in millis
double minTtl = all.Min(p => p.InitialTimeToLive) * 1000d;

if (MinimumTimout == Timeout.InfiniteTimeSpan)
{
// TODO: Log warning once?
minTtl = s_maxTimeout.TotalMilliseconds;
}
else if (MinimumTimout.HasValue && minTtl < MinimumTimout.Value.TotalMilliseconds)
{
minTtl = MinimumTimout.Value.TotalMilliseconds;
}

// max TTL check which can limit the upper boundary
if (MaximumTimeout.HasValue && MaximumTimeout != Timeout.InfiniteTimeSpan && minTtl > MaximumTimeout.Value.TotalMilliseconds)
{
minTtl = MaximumTimeout.Value.TotalMilliseconds;
}

if (minTtl < 1d)
{
return false;
}

var newEntry = new ResponseEntry(response, minTtl);

StartCleanup();
return _cache.TryAdd(key, newEntry);
}

var newEntry = new ResponseEntry(response, minTtl);

StartCleanup();
return _cache.TryAdd(key, newEntry);
}
}

Expand Down

0 comments on commit 0933a93

Please sign in to comment.