From 4d6945769d205f2a60acf13eefb5af0a2eb428fc Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Tue, 28 Apr 2026 09:16:18 -0600 Subject: [PATCH] reverseproxy: Add ability to clear dynamic upstreams cache during retries (#7662) * reverseproxy: Add ability to clear dynamic upstreams cache during retries This is an optional interface for dynamic upstream modules to implement if they cache results. TODO: More documentation; this is an experiment. * Add some godoc * Export interface; update godoc --- .../caddyhttp/reverseproxy/reverseproxy.go | 29 +++++++++++++++++++ modules/caddyhttp/reverseproxy/upstreams.go | 21 +++++++++++--- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 52d2b1ab3..cefe645ee 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -574,6 +574,17 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h // get the updated list of upstreams upstreams := h.Upstreams if h.DynamicUpstreams != nil { + if retries > 0 { + // after a failure (and thus during a retry), give dynamic upstream modules an opportunity + // to purge their relevant cache entries so we don't keep retrying bad upstreams + if cachingDynamicUpstreams, ok := h.DynamicUpstreams.(CachingUpstreamSource); ok { + if err := cachingDynamicUpstreams.ResetCache(r); err != nil { + if c := h.logger.Check(zapcore.ErrorLevel, "failed clearing dynamic upstream source's cache"); c != nil { + c.Write(zap.Error(err)) + } + } + } + } dUpstreams, err := h.DynamicUpstreams.GetUpstreams(r) if err != nil { if c := h.logger.Check(zapcore.ErrorLevel, "failed getting dynamic upstreams; falling back to static upstreams"); c != nil { @@ -1640,10 +1651,28 @@ type Selector interface { // may be called during each retry, multiple times per request, and as // such, needs to be instantaneous. The returned slice will not be // modified. +// +// For upstream sources that cache results, implement the +// [CachingUpstreamSource] interface for optimal performance. type UpstreamSource interface { GetUpstreams(*http.Request) ([]*Upstream, error) } +// CachingUpstreamSource is an upstream source that caches its upstreams. +// The relevant cache entry can be cleared/reset for a given request during +// retries if a request fails. This can help ensure that failing backends +// are not retried. +// +// EXPERIMENTAL: Subject to change. +type CachingUpstreamSource interface { + UpstreamSource + + // ResetCache clears any cache entry related to the given request. + // The next time GetUpstreams is called, it should have new upstream + // information for the given request. + ResetCache(*http.Request) error +} + // Hop-by-hop headers. These are removed when sent to the backend. // As of RFC 7230, hop-by-hop headers are required to appear in the // Connection header field. These are the headers defined by the diff --git a/modules/caddyhttp/reverseproxy/upstreams.go b/modules/caddyhttp/reverseproxy/upstreams.go index e9120725a..f7077ce78 100644 --- a/modules/caddyhttp/reverseproxy/upstreams.go +++ b/modules/caddyhttp/reverseproxy/upstreams.go @@ -119,6 +119,18 @@ func (su *SRVUpstreams) Provision(ctx caddy.Context) error { return nil } +func (su *SRVUpstreams) ResetCache(r *http.Request) error { + srvsMu.Lock() + if r == nil { + srvs = make(map[string]srvLookup) + } else { + suAddr, _, _, _ := su.expandedAddr(r) + delete(srvs, suAddr) + } + srvsMu.Unlock() + return nil +} + func (su SRVUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) { suAddr, service, proto, name := su.expandedAddr(r) @@ -554,8 +566,9 @@ var ( // Interface guards var ( - _ caddy.Provisioner = (*SRVUpstreams)(nil) - _ UpstreamSource = (*SRVUpstreams)(nil) - _ caddy.Provisioner = (*AUpstreams)(nil) - _ UpstreamSource = (*AUpstreams)(nil) + _ caddy.Provisioner = (*SRVUpstreams)(nil) + _ UpstreamSource = (*SRVUpstreams)(nil) + _ CachingUpstreamSource = (*SRVUpstreams)(nil) + _ caddy.Provisioner = (*AUpstreams)(nil) + _ UpstreamSource = (*AUpstreams)(nil) )