dnscache

package
v1.151.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package dnscache provides a local DNS cache that is safe for concurrent use, bounded in size, and uses single-flight request collapsing to avoid duplicate lookups.

The package is designed as a drop-in complement to the standard net package. It exposes LookupHost and DialContext helpers that cache resolved host names, so repeated DNS lookups for the same host return cached results and only one outstanding lookup is performed at a time.

Key features:

  • fixed-size in-memory cache with configurable capacity
  • TTL-based expiry (one cache-wide TTL for all entries) to keep DNS data fresh; authoritative DNS record TTLs are not consulted
  • host names are matched case-insensitively and an equivalent trailing dot (FQDN form) is ignored, so "Example.com", "example.com" and "example.com." share a single cache entry
  • thread-safe access for concurrent goroutines
  • single-flight behavior so duplicate lookups share one network request
  • optional custom net.Resolver support with sensible default behavior
  • DialContext helper that dials the resolved IPs sequentially in the resolver's preference order, interleaving address families (leading with the resolver-preferred family) so a dead family is not exhausted before the other is tried; family-restricted networks ("tcp4", "udp6", ...) dial only addresses of the matching family
  • configurable dialer via WithDialer, a per-attempt dial timeout via WithDialTimeout, optional client-side address rotation via WithAddressRotation, and stale-if-error resilience via WithStaleIfError

Why it matters:

  • reduces DNS resolution latency for repeated host names
  • lowers load on upstream resolvers and avoids query storms
  • keeps memory usage predictable with a capped entry count
  • makes DNS-heavy applications more resilient under concurrency
  • provides a practical http.Transport DialContext replacement for DNS-aware clients

Use this package in any Go service that performs frequent DNS lookups or needs an efficient, safe cache for host resolution.

Note: the Resolver interface is generic. Because host names are normalized to their lower-case, dot-trimmed form before lookup, a custom resolver that treats host names case-sensitively will observe the normalized name. Host names that are already IP literals bypass the resolver and the cache entirely, mirroring net.Resolver.LookupHost.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrInvalidIP = errors.New("dnscache: invalid IP address")

ErrInvalidIP is returned by Cache.DialContext when a cached address is not a valid IP literal and therefore cannot be dialed.

View Source
var ErrNoAddresses = errors.New("dnscache: no addresses resolved")

ErrNoAddresses is returned by Cache.DialContext when the resolver reports no addresses for the requested host.

Functions

This section is empty.

Types

type Cache

type Cache struct {
	// contains filtered or unexported fields
}

Cache provides DNS resolution caching with TTL and single-flight deduplication. A Cache must not be copied after first use.

func New

func New(resolver Resolver, size int, ttl time.Duration, opts ...Option) *Cache

New creates a concurrent DNS cache with TTL expiry and single-flight lookups.

If resolver is nil, a default net.Resolver is used. size bounds cache capacity (minimum effective size is 1), and ttl controls how long each hostname resolution remains valid. Behavior can be tuned with options such as WithDialer, WithDialTimeout, WithAddressRotation, and WithStaleIfError.

This constructor is useful for DNS-heavy clients that need lower latency and fewer duplicate upstream queries.

Example
package main

import (
	"fmt"
	"net/http"
	"time"

	"github.com/tecnickcom/nurago/pkg/dnscache"
)

func main() {
	// Create a DNS cache holding up to 128 hosts for 5 minutes each,
	// using the default net.Resolver.
	cache := dnscache.New(nil, 128, 5*time.Minute)

	// Wire the cache into an http.Transport so every request reuses cached
	// DNS resolutions instead of querying the resolver again.
	client := &http.Client{
		Transport: &http.Transport{
			DialContext: cache.DialContext,
		},
	}

	_ = client // use the client as usual

	fmt.Println(cache.Len())

}
Output:
0

func (*Cache) DialContext

func (c *Cache) DialContext(ctx context.Context, network, address string) (net.Conn, error)

DialContext resolves address through the cache and dials the resolved IPs.

It is intended as a drop-in replacement for transport DialContext functions (for example in http.Transport) when DNS caching is desired. Addresses are tried sequentially (not raced), in the resolver's preference order with IPv4 and IPv6 candidates interleaved (leading with the resolver-preferred family) until one connection succeeds; if every attempt fails, the individual errors are aggregated into the returned error. For family-restricted networks ("tcp4", "udp6", ...) only addresses of the matching family are dialed; if none remain, an error wrapping ErrNoAddresses is returned. Use WithAddressRotation for client-side load spreading and WithDialTimeout to bound each attempt.

func (*Cache) Len

func (c *Cache) Len() int

Len reports the current number of cached host entries.

func (*Cache) LookupHost

func (c *Cache) LookupHost(ctx context.Context, host string) ([]string, error)

LookupHost resolves host to IP addresses using cache-first semantics.

On cache miss or expiry, one goroutine performs the DNS lookup while other concurrent callers for the same host wait and share the result. Host names are matched case-insensitively and a trailing dot is ignored; an IP literal is returned as-is without a lookup.

This reduces resolver load and avoids thundering-herd lookups. The returned slice is a copy: callers may freely modify it without affecting the cached entry shared with other callers.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/tecnickcom/nurago/pkg/dnscache"
)

// staticResolver is a deterministic Resolver used to keep the examples
// reproducible; production code normally passes nil to use net.Resolver.
type staticResolver struct{}

func (staticResolver) LookupHost(_ context.Context, _ string) ([]string, error) {
	return []string{"192.0.2.1", "192.0.2.2"}, nil
}

func main() {
	cache := dnscache.New(staticResolver{}, 128, 5*time.Minute)

	// The first call queries the resolver; repeated calls within the TTL are
	// served from the cache, with concurrent lookups for the same host
	// coalesced into a single upstream query.
	addrs, err := cache.LookupHost(context.Background(), "example.com")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(addrs)
	fmt.Println(cache.Len())

}
Output:
[192.0.2.1 192.0.2.2]
1

func (*Cache) PurgeExpired

func (c *Cache) PurgeExpired() int

PurgeExpired removes all expired host entries from the cache and returns the number of entries removed. Expired entries are otherwise only removed lazily, when capacity pressure or a new lookup replaces them.

func (*Cache) Remove

func (c *Cache) Remove(host string)

Remove evicts a single host entry from the cache. The host is normalized the same way as in Cache.LookupHost, so any case or trailing-dot variant of a cached name removes the shared entry.

func (*Cache) Reset

func (c *Cache) Reset()

Reset clears all cached DNS entries.

type Option

type Option func(*config)

Option customizes a Cache created by New.

func WithAddressRotation

func WithAddressRotation() Option

WithAddressRotation rotates the order in which resolved addresses are dialed on each Cache.DialContext call, spreading connections across a host's records instead of always trying the resolver's first address. It is disabled by default so the resolver's RFC 6724 address ordering is preserved; enable it when client-side load spreading across equivalent records is desired. Rotation is applied within each address family, so the resolver-preferred family always stays first in the dial order.

func WithDialTimeout

func WithDialTimeout(timeout time.Duration) Option

WithDialTimeout bounds how long each individual address dial attempt in Cache.DialContext may take, so a single unresponsive address cannot consume the whole caller deadline before the remaining addresses are tried. It is a per-attempt timeout layered on top of the caller's context; a value <= 0 (the default) disables it, leaving only the caller's context in force. Set it generously: a value shorter than a legitimately slow connect will abort working high-latency links. A Timeout on the dialer passed to WithDialer has the same per-attempt effect; when both are configured, the shorter bound wins.

func WithDialer

func WithDialer(dialer *net.Dialer) Option

WithDialer sets the dialer used by Cache.DialContext. A nil dialer is ignored, leaving the default (keep-alive of 30s, no explicit timeout) in place. Use this to configure connection timeout, keep-alive, local address, or a Control hook, mirroring how net/http configures its transport dialer. A non-zero dialer Timeout bounds each address attempt just like WithDialTimeout; when both are configured, the shorter bound wins.

The dialer's Resolver field is unused: addresses are pre-resolved by the cache's Resolver (configured via New), and the dialer only ever receives IP literals.

func WithStaleIfError

func WithStaleIfError(maxStale time.Duration) Option

WithStaleIfError serves the last successfully resolved addresses when a later refresh fails, until maxStale past their original expiry (see sfcache.WithStaleIfError). This keeps DNS-dependent clients working through transient resolver outages. A maxStale <= 0 disables it (the default).

type Resolver

type Resolver interface {
	LookupHost(ctx context.Context, host string) (addrs []string, err error)
}

Resolver is a net.Resolver interface for DNS lookups.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL