fix(sub): coalesce external subscription refreshes (#6139)

* fix(sub): coalesce external subscription refreshes

Limit concurrent cache misses to one upstream request per URL and evict the oldest entries once the cache reaches its bounded capacity.

* fix(sub): preserve shared stale refresh results

Release every in-flight waiter on panic or error, carry the leader outcome to waiters, and strengthen cache capacity and stale fallback coverage.

---------

Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local>
This commit is contained in:
PathGao
2026-07-30 02:58:00 +08:00
committed by GitHub
parent 03cc80bb9e
commit e467b25f03
2 changed files with 194 additions and 9 deletions
+54 -9
View File
@@ -16,8 +16,9 @@ import (
// with a short timeout so a slow/dead provider can't stall a client's sub.
const (
subscriptionCacheTTL = 5 * time.Minute
subscriptionMaxBytes = 2 << 20 // 2 MiB
subscriptionCacheTTL = 5 * time.Minute
subscriptionMaxBytes = 2 << 20 // 2 MiB
subscriptionCacheCapacity = 256
)
var subscriptionHTTPClient = &http.Client{Timeout: 6 * time.Second}
@@ -27,10 +28,19 @@ type subscriptionCacheEntry struct {
fetchedAt time.Time
}
type subscriptionFetch struct {
done chan struct{}
links []string
}
var subscriptionCache = struct {
sync.Mutex
m map[string]subscriptionCacheEntry
}{m: make(map[string]subscriptionCacheEntry)}
m map[string]subscriptionCacheEntry
inflight map[string]*subscriptionFetch
}{
m: make(map[string]subscriptionCacheEntry),
inflight: make(map[string]*subscriptionFetch),
}
// fetchSubscriptionLinks returns the share links contained in a remote
// subscription URL, using a short-lived cache. On any failure it returns the
@@ -44,24 +54,59 @@ func fetchSubscriptionLinks(rawURL string) []string {
subscriptionCache.Lock()
cached, ok := subscriptionCache.m[rawURL]
subscriptionCache.Unlock()
if ok && time.Since(cached.fetchedAt) < subscriptionCacheTTL {
subscriptionCache.Unlock()
return cached.links
}
if fetch, waiting := subscriptionCache.inflight[rawURL]; waiting {
subscriptionCache.Unlock()
<-fetch.done
return fetch.links
}
fetch := &subscriptionFetch{done: make(chan struct{})}
subscriptionCache.inflight[rawURL] = fetch
subscriptionCache.Unlock()
defer func() {
subscriptionCache.Lock()
close(fetch.done)
delete(subscriptionCache.inflight, rawURL)
subscriptionCache.Unlock()
}()
links, err := doFetchSubscriptionLinks(rawURL)
if err != nil {
// Serve stale on error rather than dropping the client's configs.
if ok {
return cached.links
fetch.links = cached.links
}
return nil
return fetch.links
}
subscriptionCache.Lock()
subscriptionCache.m[rawURL] = subscriptionCacheEntry{links: links, fetchedAt: time.Now()}
trimSubscriptionCacheLocked(rawURL)
subscriptionCache.Unlock()
return links
fetch.links = links
return fetch.links
}
func trimSubscriptionCacheLocked(keep string) {
for len(subscriptionCache.m) > subscriptionCacheCapacity {
var oldestURL string
var oldest time.Time
for rawURL, entry := range subscriptionCache.m {
if rawURL == keep {
continue
}
if oldestURL == "" || entry.fetchedAt.Before(oldest) {
oldestURL = rawURL
oldest = entry.fetchedAt
}
}
if oldestURL == "" {
return
}
delete(subscriptionCache.m, oldestURL)
}
}
func doFetchSubscriptionLinks(rawURL string) ([]string, error) {