From fe47cf252c5436fa5164e8d10ae54cf8b959bb2f Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 3 Jun 2026 18:51:18 +0200 Subject: [PATCH 1/2] feat(namesys): propagate DNSLink TXT TTL The DNS resolver dropped the TXT record TTL, so a gateway could not set Cache-Control max-age for DNSLink (/ipns/) responses, and a recursive name was cached for only its final hop's TTL. - add LookupTXTWithTTLFunc and NewDNSResolverWithTTL; LookupTXTFunc and NewDNSResolver keep their signatures and report an unknown TTL (0) - carry the looked-up TTL into AsyncResult.TTL in the DNS resolver - WithDNSResolver detects a resolver implementing multiformats/go-multiaddr-dns#75 TXTWithTTLResolver and propagates the TTL; add WithDNSResolverWithTTL for a TTL-aware lookup - cap a resolved name's TTL to its shortest hop, so a DNSLink or recursive IPNS name is not cached past its earliest-expiring link Refs #329 --- CHANGELOG.md | 4 ++++ examples/go.mod | 4 ++-- examples/go.sum | 8 +++---- go.mod | 4 ++-- go.sum | 8 +++---- namesys/dns_resolver.go | 28 ++++++++++++++++++----- namesys/dns_resolver_test.go | 44 +++++++++++++++++++++++++++++++++++- namesys/namesys.go | 22 +++++++++++++++--- namesys/utilities.go | 25 ++++++++++++++++++++ 9 files changed, 125 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2a31c0da..68b4f40de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,12 @@ The following emojis are used to highlight certain changes: ### Added +- `namesys`: DNS resolution can report the TXT record TTL. `WithDNSResolver` uses it automatically when the resolver implements [`madns.TXTWithTTLResolver`](https://github.com/multiformats/go-multiaddr-dns/pull/75), and `NewDNSResolverWithTTL`, `WithDNSResolverWithTTL`, and `LookupTXTWithTTLFunc` take a TTL-aware lookup directly. A gateway can then set `Cache-Control: max-age` for DNSLink (`/ipns/`) responses from the real DNS TTL instead of a static value. `NewDNSResolver` and `LookupTXTFunc` keep their signatures and report an unknown TTL (0). [#329](https://github.com/ipfs/boxo/issues/329) + ### Changed +- `namesys`: a resolved name's TTL is the shortest across all of its hops, so a DNSLink or recursive IPNS name is not cached past its earliest-expiring link. Hops with an unknown TTL (0) are ignored, leaving single-hop and current DNSLink results unaffected. [#329](https://github.com/ipfs/boxo/issues/329) + ### Removed ### Fixed diff --git a/examples/go.mod b/examples/go.mod index cea38b548..de581f49b 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -71,7 +71,7 @@ require ( github.com/koron/go-ssdp v0.0.6 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/libp2p/go-cidranger v1.1.0 // indirect - github.com/libp2p/go-doh-resolver v0.5.0 // indirect + github.com/libp2p/go-doh-resolver v0.5.1-0.20260603151746-4e7758ec9748 // indirect github.com/libp2p/go-flow-metrics v0.3.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect github.com/libp2p/go-libp2p-kad-dht v0.40.0 // indirect @@ -91,7 +91,7 @@ require ( github.com/mr-tron/base58 v1.3.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect - github.com/multiformats/go-multiaddr-dns v0.5.0 // indirect + github.com/multiformats/go-multiaddr-dns v0.5.1-0.20260603161026-e8eb3c2de127 // indirect github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect github.com/multiformats/go-multibase v0.3.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect diff --git a/examples/go.sum b/examples/go.sum index 2ecae659d..7c7baea21 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -319,8 +319,8 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= -github.com/libp2p/go-doh-resolver v0.5.0 h1:4h7plVVW+XTS+oUBw2+8KfoM1jF6w8XmO7+skhePFdE= -github.com/libp2p/go-doh-resolver v0.5.0/go.mod h1:aPDxfiD2hNURgd13+hfo29z9IC22fv30ee5iM31RzxU= +github.com/libp2p/go-doh-resolver v0.5.1-0.20260603151746-4e7758ec9748 h1:oHfW49Wd7aQckSbLeURGHKCuqaxxazQKtOYO0a3qHBo= +github.com/libp2p/go-doh-resolver v0.5.1-0.20260603151746-4e7758ec9748/go.mod h1:ORKldXV7t2tneaa5J3KjuDN3IQqgGPVouLA4wzavwRU= github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= github.com/libp2p/go-libp2p v0.48.0 h1:h2BrLAgrj7X8bEN05K7qmrjpNHYA+6tnsGRdprjTnvo= @@ -388,8 +388,8 @@ github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= -github.com/multiformats/go-multiaddr-dns v0.5.0 h1:p/FTyHKX0nl59f+S+dEUe8HRK+i5Ow/QHMw8Nh3gPCo= -github.com/multiformats/go-multiaddr-dns v0.5.0/go.mod h1:yJ349b8TPIAANUyuOzn1oz9o22tV9f+06L+cCeMxC14= +github.com/multiformats/go-multiaddr-dns v0.5.1-0.20260603161026-e8eb3c2de127 h1:SjNba11B5BQXxoeE9qYN83CwmsHi/TApsC/0EDv3FPA= +github.com/multiformats/go-multiaddr-dns v0.5.1-0.20260603161026-e8eb3c2de127/go.mod h1:dwIQwdORZfnNQCeS7xLXyn+7626oRmMsVP30Uronhf0= github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= diff --git a/go.mod b/go.mod index 1a80b2be1..e8164df4d 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/ipld/go-codec-dagpb v1.7.0 github.com/ipld/go-ipld-prime v0.23.0 github.com/libp2p/go-buffer-pool v0.1.0 - github.com/libp2p/go-doh-resolver v0.5.0 + github.com/libp2p/go-doh-resolver v0.5.1-0.20260603151746-4e7758ec9748 github.com/libp2p/go-libp2p v0.48.0 github.com/libp2p/go-libp2p-kad-dht v0.40.0 github.com/libp2p/go-libp2p-record v0.3.1 @@ -46,7 +46,7 @@ require ( github.com/mr-tron/base58 v1.3.0 github.com/multiformats/go-base32 v0.1.0 github.com/multiformats/go-multiaddr v0.16.1 - github.com/multiformats/go-multiaddr-dns v0.5.0 + github.com/multiformats/go-multiaddr-dns v0.5.1-0.20260603161026-e8eb3c2de127 github.com/multiformats/go-multibase v0.3.0 github.com/multiformats/go-multicodec v0.10.0 github.com/multiformats/go-multihash v0.2.3 diff --git a/go.sum b/go.sum index 561bea3bf..7df7ffa2a 100644 --- a/go.sum +++ b/go.sum @@ -316,8 +316,8 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= -github.com/libp2p/go-doh-resolver v0.5.0 h1:4h7plVVW+XTS+oUBw2+8KfoM1jF6w8XmO7+skhePFdE= -github.com/libp2p/go-doh-resolver v0.5.0/go.mod h1:aPDxfiD2hNURgd13+hfo29z9IC22fv30ee5iM31RzxU= +github.com/libp2p/go-doh-resolver v0.5.1-0.20260603151746-4e7758ec9748 h1:oHfW49Wd7aQckSbLeURGHKCuqaxxazQKtOYO0a3qHBo= +github.com/libp2p/go-doh-resolver v0.5.1-0.20260603151746-4e7758ec9748/go.mod h1:ORKldXV7t2tneaa5J3KjuDN3IQqgGPVouLA4wzavwRU= github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= github.com/libp2p/go-libp2p v0.48.0 h1:h2BrLAgrj7X8bEN05K7qmrjpNHYA+6tnsGRdprjTnvo= @@ -385,8 +385,8 @@ github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= -github.com/multiformats/go-multiaddr-dns v0.5.0 h1:p/FTyHKX0nl59f+S+dEUe8HRK+i5Ow/QHMw8Nh3gPCo= -github.com/multiformats/go-multiaddr-dns v0.5.0/go.mod h1:yJ349b8TPIAANUyuOzn1oz9o22tV9f+06L+cCeMxC14= +github.com/multiformats/go-multiaddr-dns v0.5.1-0.20260603161026-e8eb3c2de127 h1:SjNba11B5BQXxoeE9qYN83CwmsHi/TApsC/0EDv3FPA= +github.com/multiformats/go-multiaddr-dns v0.5.1-0.20260603161026-e8eb3c2de127/go.mod h1:dwIQwdORZfnNQCeS7xLXyn+7626oRmMsVP30Uronhf0= github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= diff --git a/namesys/dns_resolver.go b/namesys/dns_resolver.go index 43f85b90d..e7268704a 100644 --- a/namesys/dns_resolver.go +++ b/namesys/dns_resolver.go @@ -20,15 +20,31 @@ import ( // LookupTXTFunc is a function that lookups TXT record values. type LookupTXTFunc func(ctx context.Context, name string) (txt []string, err error) +// LookupTXTWithTTLFunc is like [LookupTXTFunc] but also returns how long the TXT +// records may be cached. A TTL of 0 means the TTL is unknown. +type LookupTXTWithTTLFunc func(ctx context.Context, name string) (txt []string, ttl time.Duration, err error) + // DNSResolver implements [Resolver] on DNS domains. type DNSResolver struct { - lookupTXT LookupTXTFunc + lookupTXT LookupTXTWithTTLFunc } var _ Resolver = &DNSResolver{} -// NewDNSResolver constructs a name resolver using DNS TXT records. +// NewDNSResolver constructs a name resolver from DNS TXT records. It reports an +// unknown TTL (0) for every result; use [NewDNSResolverWithTTL] when the lookup +// can report real TTLs. func NewDNSResolver(lookup LookupTXTFunc) *DNSResolver { + return &DNSResolver{lookupTXT: func(ctx context.Context, name string) ([]string, time.Duration, error) { + txt, err := lookup(ctx, name) + return txt, 0, err + }} +} + +// NewDNSResolverWithTTL is like [NewDNSResolver] but takes a lookup that reports +// each record's TTL. The TTL flows into the resolved result, so a gateway can +// set Cache-Control max-age from a DNSLink's TTL. +func NewDNSResolverWithTTL(lookup LookupTXTWithTTLFunc) *DNSResolver { return &DNSResolver{lookupTXT: lookup} } @@ -85,7 +101,7 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, p path.Path, options } if subRes.Err == nil { p, err := joinPaths(subRes.Path, p) - emitOnceResult(ctx, out, AsyncResult{Path: p, LastMod: time.Now(), Err: err}) + emitOnceResult(ctx, out, AsyncResult{Path: p, TTL: subRes.TTL, LastMod: time.Now(), Err: err}) // Return without waiting for rootRes, since this result // (for "_dnslink."+fqdn) takes precedence } else { @@ -107,7 +123,7 @@ func workDomain(ctx context.Context, r *DNSResolver, name string, res chan Async defer close(res) - txt, err := r.lookupTXT(ctx, name) + txt, ttl, err := r.lookupTXT(ctx, name) if err != nil { var dnsErr *net.DNSError if errors.As(err, &dnsErr) { @@ -142,8 +158,8 @@ func workDomain(ctx context.Context, r *DNSResolver, name string, res chan Async // There were no TXT records with a dnslink res <- AsyncResult{Err: ErrMissingDNSLinkRecord} case 1: - // Found 1 valid! Return it. - res <- AsyncResult{Path: paths[0]} + // Found 1 valid! Return it with the record TTL. + res <- AsyncResult{Path: paths[0], TTL: ttl} default: // Found more than 1 IPFS/IPNS path. res <- AsyncResult{Err: ErrMultipleDNSLinkRecords} diff --git a/namesys/dns_resolver_test.go b/namesys/dns_resolver_test.go index c174a590a..1bf971e60 100644 --- a/namesys/dns_resolver_test.go +++ b/namesys/dns_resolver_test.go @@ -4,6 +4,7 @@ import ( "context" "net" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -152,7 +153,7 @@ func newMockDNS() *mockDNS { func TestDNSResolution(t *testing.T) { t.Parallel() - r := &DNSResolver{lookupTXT: newMockDNS().lookupTXT} + r := NewDNSResolver(newMockDNS().lookupTXT) for _, testCase := range []struct { name string @@ -206,3 +207,44 @@ func TestDNSResolution(t *testing.T) { }) } } + +func TestDNSResolutionWithTTL(t *testing.T) { + t.Parallel() + + const ttl = 42 * time.Second + lookup := func(ctx context.Context, name string) ([]string, time.Duration, error) { + if name == "_dnslink.ttl.example.com." { + return []string{"dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD"}, ttl, nil + } + return nil, 0, &net.DNSError{IsNotFound: true} + } + + // the TTL-aware resolver propagates the record TTL to the resolved result + r := NewDNSResolverWithTTL(lookup) + testResolution(t, r, "/ipns/ttl.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", ttl, nil) + + // the legacy constructor reports an unknown TTL (0) for the same lookup + rNoTTL := NewDNSResolver(func(ctx context.Context, name string) ([]string, error) { + txt, _, err := lookup(ctx, name) + return txt, err + }) + testResolution(t, rNoTTL, "/ipns/ttl.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 0, nil) +} + +func TestMinNonZeroTTL(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + a, b, want time.Duration + }{ + {0, 0, 0}, // both unknown -> unknown + {0, 5 * time.Second, 5 * time.Second}, // one unknown -> the other + {5 * time.Second, 0, 5 * time.Second}, // one unknown -> the other + {3 * time.Second, 5 * time.Second, 3 * time.Second}, // both known -> min + {5 * time.Second, 3 * time.Second, 3 * time.Second}, // both known -> min + } { + if got := minNonZeroTTL(tc.a, tc.b); got != tc.want { + t.Fatalf("minNonZeroTTL(%s, %s) = %s; want %s", tc.a, tc.b, got, tc.want) + } + } +} diff --git a/namesys/namesys.go b/namesys/namesys.go index 0f4084723..eff35e6ec 100644 --- a/namesys/namesys.go +++ b/namesys/namesys.go @@ -91,11 +91,27 @@ func WithMaxCacheTTL(dur time.Duration) Option { } } -// WithDNSResolver is an option that supplies a custom DNS resolver to use instead -// of the system default. +// WithDNSResolver sets a custom DNS resolver in place of the system default. If +// that resolver also implements [madns.TXTWithTTLResolver], its TXT TTLs flow +// into resolved results, and from there into the gateway's Cache-Control header. func WithDNSResolver(rslv madns.BasicResolver) Option { return func(ns *namesys) error { - ns.dnsResolver = NewDNSResolver(rslv.LookupTXT) + // A resolver that reports TXT TTLs (such as a DoH resolver via + // multiformats/go-multiaddr-dns#75) carries the DNSLink TTL through. + if ttlRslv, ok := rslv.(madns.TXTWithTTLResolver); ok { + ns.dnsResolver = NewDNSResolverWithTTL(ttlRslv.LookupTXTWithTTL) + } else { + ns.dnsResolver = NewDNSResolver(rslv.LookupTXT) + } + return nil + } +} + +// WithDNSResolverWithTTL is like [WithDNSResolver] but takes a lookup that +// reports TXT TTLs directly. +func WithDNSResolverWithTTL(lookup LookupTXTWithTTLFunc) Option { + return func(ns *namesys) error { + ns.dnsResolver = NewDNSResolverWithTTL(lookup) return nil } } diff --git a/namesys/utilities.go b/namesys/utilities.go index ca72316c1..12459bc2d 100644 --- a/namesys/utilities.go +++ b/namesys/utilities.go @@ -3,6 +3,7 @@ package namesys import ( "context" "strings" + "time" "github.com/ipfs/boxo/path" "go.opentelemetry.io/otel" @@ -46,6 +47,11 @@ func resolveAsync(ctx context.Context, r resolver, p path.Path, options ResolveO var subCh <-chan AsyncResult var cancelSub context.CancelFunc + // parentTTL is the TTL of the mutable result that started this recursion, + // for example a DNSLink that points at an IPNS name. Each sub-result's TTL + // is held to the shorter of the two, so a name is never cached past its + // shortest hop. + var parentTTL time.Duration defer func() { if cancelSub != nil { cancelSub() @@ -92,6 +98,7 @@ func resolveAsync(ctx context.Context, r resolver, p path.Path, options ResolveO subCtx, cancelSub = context.WithCancel(ctx) _ = cancelSub + parentTTL = res.TTL subCh = resolveAsync(subCtx, r, res.Path, subOpts) case res, ok := <-subCh: if !ok { @@ -99,6 +106,10 @@ func resolveAsync(ctx context.Context, r resolver, p path.Path, options ResolveO break } + // Keep the shorter of the parent (e.g. DNSLink) and sub-result + // TTLs so the result is never cached past its shortest hop. + res.TTL = minNonZeroTTL(parentTTL, res.TTL) + // We don't bother returning here in case of context timeout as there is // no good reason to do that, and we may still be able to emit a result emitResult(ctx, outCh, res) @@ -120,6 +131,20 @@ func emitResult(ctx context.Context, outCh chan<- AsyncResult, r AsyncResult) { } } +// minNonZeroTTL returns the shorter of two TTLs, treating 0 as unknown and +// ignoring it. If both are unknown, it returns 0. The builtin min won't do +// here: it would pick the 0. +func minNonZeroTTL(a, b time.Duration) time.Duration { + switch { + case a <= 0: + return b + case b <= 0: + return a + default: + return min(a, b) + } +} + func joinPaths(resolvedBase, unresolvedPath path.Path) (path.Path, error) { if resolvedBase == nil { return nil, nil From 4a82052afb1a2a807b63a619098471c2a13c4426 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Thu, 4 Jun 2026 00:50:07 +0200 Subject: [PATCH 2/2] refactor(namesys): simplify minNonZeroTTL drop the min from negative inputs so a negative TTL is never returned; cover the negative cases in the test table. --- namesys/dns_resolver_test.go | 13 ++++++++----- namesys/utilities.go | 17 +++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/namesys/dns_resolver_test.go b/namesys/dns_resolver_test.go index 1bf971e60..fa65c7ad9 100644 --- a/namesys/dns_resolver_test.go +++ b/namesys/dns_resolver_test.go @@ -237,11 +237,14 @@ func TestMinNonZeroTTL(t *testing.T) { for _, tc := range []struct { a, b, want time.Duration }{ - {0, 0, 0}, // both unknown -> unknown - {0, 5 * time.Second, 5 * time.Second}, // one unknown -> the other - {5 * time.Second, 0, 5 * time.Second}, // one unknown -> the other - {3 * time.Second, 5 * time.Second, 3 * time.Second}, // both known -> min - {5 * time.Second, 3 * time.Second, 3 * time.Second}, // both known -> min + {0, 0, 0}, // both unknown -> unknown + {0, 5 * time.Second, 5 * time.Second}, // one unknown -> the other + {5 * time.Second, 0, 5 * time.Second}, // one unknown -> the other + {3 * time.Second, 5 * time.Second, 3 * time.Second}, // both known -> min + {5 * time.Second, 3 * time.Second, 3 * time.Second}, // both known -> min + {-1 * time.Second, 5 * time.Second, 5 * time.Second}, // negative ignored -> the other + {-3 * time.Second, -5 * time.Second, 0}, // both negative -> never negative + {-1 * time.Second, 0, 0}, // negative and unknown -> never negative } { if got := minNonZeroTTL(tc.a, tc.b); got != tc.want { t.Fatalf("minNonZeroTTL(%s, %s) = %s; want %s", tc.a, tc.b, got, tc.want) diff --git a/namesys/utilities.go b/namesys/utilities.go index 12459bc2d..1c80753e8 100644 --- a/namesys/utilities.go +++ b/namesys/utilities.go @@ -131,18 +131,15 @@ func emitResult(ctx context.Context, outCh chan<- AsyncResult, r AsyncResult) { } } -// minNonZeroTTL returns the shorter of two TTLs, treating 0 as unknown and -// ignoring it. If both are unknown, it returns 0. The builtin min won't do -// here: it would pick the 0. +// minNonZeroTTL returns the shorter of two TTLs, treating a non-positive value +// as unknown and ignoring it. If both are unknown it returns 0, and a negative +// input is never returned. func minNonZeroTTL(a, b time.Duration) time.Duration { - switch { - case a <= 0: - return b - case b <= 0: - return a - default: - return min(a, b) + ttl := min(a, b) + if ttl <= 0 { + ttl = max(0, a, b) } + return ttl } func joinPaths(resolvedBase, unresolvedPath path.Path) (path.Path, error) {