From 90c498233bc85602e282ff5d3a44b66f82b16a3a Mon Sep 17 00:00:00 2001 From: Shekhar Vaidya Date: Mon, 18 May 2026 23:12:59 +0530 Subject: [PATCH] fix: route background stale refresh through singleflight; cleanup logging - Background refresh on stale serve now goes through sfGroup.Do so concurrent stale hits collapse into one Microsoft call instead of hammering the API from multiple goroutines simultaneously. - cleanupCaches: only logs when entries are actually evicted (was logging every 30min regardless of whether anything changed). Co-Authored-By: Claude Sonnet 4.6 --- backend/main.go | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/backend/main.go b/backend/main.go index 2b1e6f9..695a67a 100644 --- a/backend/main.go +++ b/backend/main.go @@ -829,16 +829,21 @@ func handleProxy(w http.ResponseWriter, r *http.Request) { atomic.AddInt64(&mLinkStale, 1) log.Printf("/proxy: product_id=%s sku_id=%s -> fetch failed (%v), serving stale\n", productID, skuID, err) go func() { - raw, bgErr := fetchDownloadLinksFromMS(productID, skuID) - if bgErr != nil { - log.Printf("/proxy: background refresh failed for %s:%s: %v\n", productID, skuID, bgErr) - return - } - exp := parseLinkExpiry(raw) - linkCacheMu.Lock() - linkCache[cacheKey] = linkCacheEntry{RawJSON: raw, ExpiresAt: exp, FetchedAt: time.Now()} - linkCacheMu.Unlock() - log.Printf("/proxy: background refresh succeeded for %s:%s, cached until %s\n", productID, skuID, exp.Format(time.RFC3339)) + // Use singleflight so concurrent stale serves don't all hit Microsoft + sfBgKey := "link:" + cacheKey + sfGroup.Do(sfBgKey, func() (interface{}, error) { + raw, bgErr := fetchDownloadLinksFromMS(productID, skuID) + if bgErr != nil { + log.Printf("/proxy: background refresh failed for %s:%s: %v\n", productID, skuID, bgErr) + return nil, bgErr + } + exp := parseLinkExpiry(raw) + linkCacheMu.Lock() + linkCache[cacheKey] = linkCacheEntry{RawJSON: raw, ExpiresAt: exp, FetchedAt: time.Now()} + linkCacheMu.Unlock() + log.Printf("/proxy: background refresh succeeded for %s:%s, cached until %s\n", productID, skuID, exp.Format(time.RFC3339)) + return nil, nil + }) }() w.Write(cached.RawJSON) return @@ -943,11 +948,13 @@ func cleanupCaches() { ticker := time.NewTicker(30 * time.Minute) for range ticker.C { now := time.Now() + var skuDel, linkDel, negDel int skuCacheMu.Lock() for k, v := range skuCache { if now.After(v.ExpiresAt) { delete(skuCache, k) + skuDel++ } } skuCacheMu.Unlock() @@ -957,6 +964,7 @@ func cleanupCaches() { for k, v := range linkCache { if now.After(v.ExpiresAt.Add(4 * time.Hour)) { delete(linkCache, k) + linkDel++ } } linkCacheMu.Unlock() @@ -965,11 +973,14 @@ func cleanupCaches() { for k, v := range negCache { if now.After(v.ExpiresAt) { delete(negCache, k) + negDel++ } } negCacheMu.Unlock() - log.Printf("cache cleanup: sku=%d link=%d neg=%d\n", len(skuCache), len(linkCache), len(negCache)) + if skuDel+linkDel+negDel > 0 { + log.Printf("cache cleanup: evicted sku=%d link=%d neg=%d\n", skuDel, linkDel, negDel) + } } }