Skip to content

Commit

Permalink
feat: add in-memory cache for DNS
Browse files Browse the repository at this point in the history
  • Loading branch information
link2xt committed Oct 20, 2024
1 parent 5e58bf7 commit dc64813
Showing 1 changed file with 119 additions and 10 deletions.
129 changes: 119 additions & 10 deletions src/net/dns.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,44 @@
//! DNS resolution and cache.
//!
//! DNS cache in Delta Chat has two layers:
//! in-memory cache and persistent `dns_cache` SQL table.
//!
//! In-memory cache is using a "stale-while-revalidate" strategy.
//! If there is a cached value, it is returned immediately
//! and revalidation task is started in the background
//! to replace old cached IP addresses with new ones.
//! If there is no cached value yet,
//! lookup only finishes when `lookup_host` returns first results.
//! In-memory cache is shared between all accounts
//! and is never stored on the disk.
//! It can be thought of as an extension
//! of the system resolver.
//!
//! Persistent `dns_cache` SQL table is used to collect
//! all IP addresses ever seen for the hostname
//! together with the timestamp
//! of the last time IP address has been seen.
//! Note that this timestamp reflects the time
//! IP address was returned by the in-memory cache
//! rather than the underlying system resolver.
//! Unused entries are removed after 30 days
//! (`CACHE_TTL` constant) to avoid having
//! old non-working IP addresses in the cache indefinitely.
//!
//! When Delta Chat needs an IP address for the host,
//! it queries in-memory cache for the next result
//! and merges the list of IP addresses
//! with the list of IP addresses from persistent cache.
//! Resulting list is constructed
//! by taking the first two results from the resolver
//! followed up by persistent cache results
//! and terminated by the rest of resolver results.
//!
//! Persistent cache results are sorted
//! by the time of the most recent successful connection
//! using the result. For results that have never been
//! used for successful connection timestamp of
//! retrieving them from in-memory cache is used.

use anyhow::{Context as _, Result};
use std::collections::HashMap;
Expand Down Expand Up @@ -42,33 +82,102 @@ pub(crate) async fn prune_dns_cache(context: &Context) -> Result<()> {
Ok(())
}

/// Looks up the hostname and updates DNS cache
/// on success.
/// Map from hostname to IP addresses.
static LOOKUP_HOST_CACHE: Lazy<std::sync::Mutex<HashMap<String, Vec<IpAddr>>>> =
Lazy::new(Default::default);

async fn lookup_host_with_memory_cache(
context: &Context,
hostname: &str,
port: u16,
) -> Result<Vec<IpAddr>> {
let stale_result = LOOKUP_HOST_CACHE.lock().unwrap().get(hostname).cloned();
if let Some(stale_result) = stale_result {
// Revalidate the cache in the background.
{
let context = context.clone();
let hostname = hostname.to_string();
let hostname_clone = hostname.clone();
tokio::spawn(async move {
match lookup_host((hostname.as_str(), port))
.await
.context("DNS lookup failure")
{
Ok(res) => {
let res = res.into_iter().map(|addr| addr.ip()).collect();
LOOKUP_HOST_CACHE
.lock()
.unwrap()
.insert(hostname_clone, res);
}
Err(err) => {
warn!(
context,
"Failed to revalidate results for {hostname:?}: {err:#}."
);
}
}
});
}

info!(context, "Using stale DNS resolution for {hostname}.");
Ok(stale_result)
} else {
info!(
context,
"No stale DNS resolution for {hostname} available, waiting for the resolver."
);
let res: Vec<IpAddr> = lookup_host((hostname, port))
.await
.context("DNS lookup failure")?
.map(|addr| addr.ip())
.collect();

// Insert initial result into cache.
//
// There may already be a result from a parallel
// task stored, overwriting it is not a problem.
LOOKUP_HOST_CACHE
.lock()
.unwrap()
.insert(hostname.to_string(), res.clone());
Ok(res)
}
}

/// Looks up the hostname and updates
/// persistent DNS cache on success.
async fn lookup_host_and_update_cache(
context: &Context,
hostname: &str,
port: u16,
now: i64,
) -> Result<Vec<SocketAddr>> {
let res: Vec<SocketAddr> = timeout(super::TIMEOUT, lookup_host((hostname, port)))
.await
.context("DNS lookup timeout")?
.context("DNS lookup failure")?
.collect();
let res: Vec<IpAddr> = timeout(
super::TIMEOUT,
lookup_host_with_memory_cache(context, hostname, port),
)
.await
.context("DNS lookup timeout")?
.context("DNS lookup with memory cache failure")?;

for addr in &res {
let ip_string = addr.ip().to_string();
for ip in &res {
let ip_string = ip.to_string();
if ip_string == hostname {
// IP address resolved into itself, not interesting to cache.
continue;
}

info!(context, "Resolved {hostname}:{port} into {addr}.");
info!(context, "Resolved {hostname} into {ip}.");

// Update the cache.
update_cache(context, hostname, &ip_string, now).await?;
}

let res = res
.into_iter()
.map(|ip| SocketAddr::new(ip, port))
.collect();
Ok(res)
}

Expand Down

0 comments on commit dc64813

Please sign in to comment.