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.
It exposes Cache.LookupHost and Cache.DialContext, a drop-in replacement for an http.Transport DialContext.
Caching ¶
Resolved host names are cached for one cache-wide TTL; the authoritative DNS record TTLs are not consulted. Concurrent callers asking for the same host share a single lookup.
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 entry. A custom Resolver therefore observes the normalized name. Host names that are already IP literals bypass the resolver and the cache entirely, mirroring net.Resolver.LookupHost.
The configured size bounds the number of cached address sets, never the number of distinct hosts looked up; Cache.Len can exceed it.
Dialing ¶
Cache.DialContext dials the resolved IPs sequentially (not raced) in the resolver's preference order, interleaving address families (leading with the resolver-preferred one) so a dead family is not exhausted before the other is tried. Family-restricted networks ("tcp4", "udp6", ...) dial only addresses of the matching family.
WithDialer configures the dialer, WithDialTimeout bounds each attempt, and WithAddressRotation spreads connections across a host's records.
Stale-if-error ¶
WithStaleOnFailure serves the last successfully resolved addresses for a window measured from the failed refresh, so rarely resolved hosts are protected too. WithStaleIfError is the RFC 5861 variant, whose window is measured from the addresses' original expiry.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
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.
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 ¶
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, WithStaleOnFailure, and WithStaleIfError.
A size <= 0 is clamped to a capacity of 1. A ttl <= 0 disables caching entirely (every call queries the resolver) while still coalescing concurrent lookups for the same host: a zero ttl means "never cache", not "cache forever".
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 ¶
DialContext resolves address through the cache and dials the resolved IPs.
It is a drop-in replacement for a transport DialContext (for example in http.Transport). 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.
func (*Cache) Len ¶
Len reports the current number of cached host entries.
It is not the cache's occupancy against its capacity: it also counts the hosts being resolved and the residue of a failed resolution, neither of which holds addresses, so it can exceed the configured size. The overrun is bounded by the caller's own request concurrency plus two, never by the number of distinct hosts looked up.
During an outage, a stale serve that could evict nothing holds one address set more (see WithStaleOnFailure). It is reclaimed by the next host successfully resolved.
func (*Cache) LookupHost ¶
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.
The returned slice is a copy: callers may freely modify it without affecting the cached entry shared with other callers.
With WithStaleOnFailure or WithStaleIfError enabled, a failed refresh returns the last successfully resolved addresses with a NIL error: callers cannot tell a stale answer from a fresh one.
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 ¶
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.
NOTE: it also removes the addresses retained by WithStaleOnFailure or WithStaleIfError, forfeiting stale protection for those hosts until the next successful resolution. Calling it on a timer therefore voids the outage protection those options provide.
func (*Cache) Remove ¶
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.
A resolution in flight for the host is invalidated: its result is returned to the caller that started it but not cached, and callers waiting on it are released to resolve again, so a second concurrent resolver call for that host may start alongside the one it superseded.
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.
Rotation is applied within each address family, so the resolver-preferred family always stays first in the dial order.
func WithDialTimeout ¶
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 layered on top of the caller's context; a value <= 0 (the default) disables it.
NOTE: 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 ¶
WithDialer sets the dialer used by Cache.DialContext, configuring the connection timeout, keep-alive, local address, or a Control hook. A nil dialer is ignored, leaving the default (keep-alive of 30s, no explicit timeout) in place.
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 ¶
WithStaleIfError serves the last successfully resolved addresses when a later refresh fails, until maxStale past their original expiry (RFC 5861 stale-if-error, see github.com/tecnickcom/nurago/pkg/sfcache.Config.MaxStale). A maxStale <= 0 disables it (the default).
PRECONDITION: because the window is anchored to the expiry and not to the failure, only a host resolved more often than ttl + maxStale is protected. A host that has been idle for longer than ttl + maxStale has no stale protection at all: the resolver error is returned even though its last known good addresses are still cached. Use WithStaleOnFailure to protect rarely resolved hosts too.
func WithStaleOnFailure ¶ added in v1.152.0
WithStaleOnFailure serves the last successfully resolved addresses for up to maxStaleOnFailure after a refresh first fails, however long the host had been idle before the failure (see github.com/tecnickcom/nurago/pkg/sfcache.Config.MaxStaleOnFailure).
Unlike WithStaleIfError, it also protects hosts that are resolved rarely. The window is anchored once, by the first failed refresh: further failures keep serving the same addresses until that deadline but never push it back, so a broken resolver cannot pin an address set forever. Every call still attempts a fresh resolution and recovery is automatic on the first success.
A maxStaleOnFailure <= 0 disables it (the default). When both options are set, the addresses are served stale until the later of the two deadlines.