diff --git a/lib/discover/cache.go b/lib/discover/cache.go index 0ccfab1fe..ae6b91c2c 100644 --- a/lib/discover/cache.go +++ b/lib/discover/cache.go @@ -44,6 +44,13 @@ type prioritizedAddress struct { addr string } +// An error may implement cachedError, in which case it will be interrogated +// to see how long we should cache the error. This overrides the default +// negative cache time. +type cachedError interface { + CacheFor() time.Duration +} + func NewCachingMux() *CachingMux { return &CachingMux{ Supervisor: suture.NewSimple("discover.cachingMux"), @@ -84,10 +91,11 @@ func (m *CachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays continue } - if !cacheEntry.found && time.Since(cacheEntry.when) < finder.negCacheTime { + valid := time.Now().Before(cacheEntry.validUntil) || time.Since(cacheEntry.when) < finder.negCacheTime + if !cacheEntry.found && valid { // It's a negative, valid entry. We should not make another // attempt right now. - l.Debugln("negative cache entry for", deviceID, "at", finder) + l.Debugln("negative cache entry for", deviceID, "at", finder, "valid until", cacheEntry.when.Add(finder.negCacheTime), "or", cacheEntry.validUntil) continue } @@ -111,10 +119,14 @@ func (m *CachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays }) } else { // Lookup returned error, add a negative cache entry. - m.caches[i].Set(deviceID, CacheEntry{ + entry := CacheEntry{ when: time.Now(), found: false, - }) + } + if err, ok := err.(cachedError); ok { + entry.validUntil = time.Now().Add(err.CacheFor()) + } + m.caches[i].Set(deviceID, entry) } } m.mut.Unlock() diff --git a/lib/discover/discover.go b/lib/discover/discover.go index 82bd2b7cb..da27d84ee 100644 --- a/lib/discover/discover.go +++ b/lib/discover/discover.go @@ -22,10 +22,11 @@ type Finder interface { } type CacheEntry struct { - Direct []string `json:"direct"` - Relays []Relay `json:"relays"` - when time.Time // When did we get the result - found bool // Is it a success (cacheTime applies) or a failure (negCacheTime applies)? + Direct []string `json:"direct"` + Relays []Relay `json:"relays"` + when time.Time // When did we get the result + found bool // Is it a success (cacheTime applies) or a failure (negCacheTime applies)? + validUntil time.Time // Validity time, overrides normal calculation } // A FinderService is a Finder that has background activity and must be run as diff --git a/lib/discover/global.go b/lib/discover/global.go index f3b180edd..61ac98b67 100644 --- a/lib/discover/global.go +++ b/lib/discover/global.go @@ -56,6 +56,16 @@ type serverOptions struct { id string // expected server device ID } +// A lookupError is any other error but with a cache validity time attached. +type lookupError struct { + error + cacheFor time.Duration +} + +func (e lookupError) CacheFor() time.Duration { + return e.cacheFor +} + func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, relayStat RelayStatusProvider) (FinderService, error) { server, opts, err := parseOptions(server) if err != nil { @@ -138,11 +148,16 @@ func (c *globalClient) Lookup(device protocol.DeviceID) (direct []string, relays if resp.StatusCode != 200 { resp.Body.Close() l.Debugln("globalClient.Lookup", qURL, resp.Status) - return nil, nil, errors.New(resp.Status) + err := errors.New(resp.Status) + if secs, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && secs > 0 { + err = lookupError{ + error: err, + cacheFor: time.Duration(secs) * time.Second, + } + } + return nil, nil, err } - // TODO: Handle 429 and Retry-After? - var ann announcement err = json.NewDecoder(resp.Body).Decode(&ann) resp.Body.Close()