Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 31 additions & 21 deletions linkerd/dns/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use thiserror::Error;
use tokio::time::{self, Instant};
use tracing::{debug, trace};

pub mod minimum_ttl;

#[derive(Clone)]
pub struct Resolver {
dns: TokioResolver,
Expand Down Expand Up @@ -201,32 +203,40 @@ impl fmt::Debug for Resolver {
// === impl ResolveError ===

impl ResolveError {
/// Returns the amount of time that the resolver should wait before
/// retrying.
/// Returns the amount of time that the resolver should wait before retrying.
pub fn negative_ttl(&self) -> Option<time::Duration> {
if let Some(hickory_resolver::proto::ProtoErrorKind::NoRecordsFound {
negative_ttl: Some(ttl_secs),
..
}) = self
.a_error
.0
.proto()
.map(hickory_resolver::proto::ProtoError::kind)
{
return Some(time::Duration::from_secs(*ttl_secs as u64));
let Self {
a_error: ARecordError(a_error),
srv_error,
} = self;

if let ttl @ Some(_) = Self::negative_ttl_of(a_error) {
return ttl;
}

if let SrvRecordError::Resolve(error) = &self.srv_error {
if let Some(hickory_resolver::proto::ProtoErrorKind::NoRecordsFound {
negative_ttl: Some(ttl_secs),
..
}) = error.proto().map(hickory_resolver::proto::ProtoError::kind)
{
return Some(time::Duration::from_secs(*ttl_secs as u64));
}
match srv_error {
SrvRecordError::Resolve(srv_error) => Self::negative_ttl_of(srv_error),
SrvRecordError::Invalid(_) => None,
}
}

/// Returns the negative TTL [`time::Duration`] of a [`hickory_resolver::ResolveError`].
///
/// This function will defensively enforce a minimum negative TTL.
fn negative_ttl_of(error: &hickory_resolver::ResolveError) -> Option<time::Duration> {
use hickory_resolver::proto::{ProtoError, ProtoErrorKind};

None
let Some(ProtoErrorKind::NoRecordsFound {
negative_ttl: Some(ttl_secs),
..
}) = error.proto().map(ProtoError::kind)
else {
return None;
};

let ttl = time::Duration::from_secs(*ttl_secs as u64);
let ttl = minimum_ttl::with_minimum_duration(ttl);
Some(ttl)
}
}

Expand Down
43 changes: 43 additions & 0 deletions linkerd/dns/src/minimum_ttl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//! Minimum TTL enforcement.
//!
//! This module contains functions to enforce a lower-bound for TTL's (including negative TTL's
//! used during error recovery) to prevent DNS resolution from spinning in a hot-loop.

use tokio::time::{Duration, Instant};
use tracing::debug;

/// The minimum TTL duration that will be respected.
const MINIMUM_TTL: Duration = Duration::from_secs(5);

/// Apply a lower-bound to the given [`Instant`].
///
/// NB: This enforces a lower-bound for TTL's to prevent DNS resolution from spinning in a
/// hot-loop.
#[tracing::instrument(level = "debug")]
pub fn with_minimum_expiry(valid_until: Instant) -> Instant {
let minimum = Instant::now() + MINIMUM_TTL;

// Choose a deadline; if the expiry is too short, fall back to the minimum TTL.
let deadline = if valid_until >= minimum {
valid_until
} else {
debug!(ttl.min = ?MINIMUM_TTL, "Given TTL too short, using a minimum TTL");
minimum
};

deadline
}

/// Apply a lower-bound to the given [`Duration`].
///
/// NB: This enforces a lower-bound for negative TTL's to prevent DNS resolution error recovery
/// from spinning in a hot-loop.
pub fn with_minimum_duration(ttl: Duration) -> Duration {
if ttl < MINIMUM_TTL {
// Choose a deadline; if the expiry is too short, fall back to the minimum TTL.
debug!(ttl.min = ?MINIMUM_TTL, ?ttl, "Given Negative TTL too short, using a minimum TTL");
return MINIMUM_TTL;
}

ttl
}
29 changes: 4 additions & 25 deletions linkerd/proxy/dns-resolve/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ impl<T: Param<Addr>> tower::Service<T> for DnsResolve {
}

async fn resolution(dns: dns::Resolver, na: NameAddr) -> Result<UpdateStream, Error> {
use linkerd_dns::minimum_ttl::with_minimum_expiry;
use tokio::time::sleep_until;
use tokio_stream::wrappers::ReceiverStream;

// Don't return a stream before the initial resolution completes. Then,
Expand All @@ -80,7 +82,7 @@ async fn resolution(dns: dns::Resolver, na: NameAddr) -> Result<UpdateStream, Er
trace!("Closed");
return;
}
sleep_until_expired(expiry).await;
sleep_until(with_minimum_expiry(expiry)).await;

loop {
match dns.resolve_addrs(na.name().as_ref(), na.port()).await {
Expand All @@ -91,7 +93,7 @@ async fn resolution(dns: dns::Resolver, na: NameAddr) -> Result<UpdateStream, Er
trace!("Closed");
return;
}
sleep_until_expired(expiry).await;
sleep_until(with_minimum_expiry(expiry)).await;
}
Err(error) => {
debug!(%error);
Expand All @@ -107,26 +109,3 @@ async fn resolution(dns: dns::Resolver, na: NameAddr) -> Result<UpdateStream, Er

Ok(Box::pin(ReceiverStream::new(rx)))
}

/// Sleep for the provided [`Duration`][tokio::time::Duration].
///
/// NB: This enforces a lower-bound for TTL's to prevent [`resolution()`], above, from spinning
/// in a hot-loop.
#[tracing::instrument(level = "debug")]
async fn sleep_until_expired(valid_until: tokio::time::Instant) {
use tokio::time::{sleep_until, Duration, Instant};

/// The minimum TTL duration that will be respected.
const MINIMUM_TTL: Duration = Duration::from_secs(5);
let minimum = Instant::now() + MINIMUM_TTL;

// Choose a deadline; if the expiry is too short, fall back to the minimum TTL.
let deadline = if valid_until >= minimum {
valid_until
} else {
debug!(ttl.min = ?MINIMUM_TTL, "Given TTL too short, using a minimum TTL");
minimum
};

sleep_until(deadline).await;
}
Loading