feat: /metrics endpoint, cache eviction, and observability fixes #16

Merged
starkSV merged 2 commits from feat/metrics into main 2026-05-18 23:24:07 +05:30
starkSV commented 2026-05-18 23:14:48 +05:30 (Migrated from github.com)

Summary

  • /metrics endpoint — auth-protected (METRICS_SECRET env var) JSON endpoint exposing per-endpoint request counts, cache hits, MS fetch counts, neg-cache hits, stale serves, hit rates, and live cache sizes
  • Cache evictioncleanupCaches() goroutine runs every 30min, evicts expired SKU/neg entries and link entries past ExpiresAt + 4h stale window — fixes unbounded map growth
  • Local rand source — package-level rand.Rand replaces global rand to avoid mutex contention at high QPS
  • Clearer singleflight keyssfKey separated from negKey in both handlers
  • Background refresh via singleflight — stale serve triggers a background goroutine that goes through sfGroup.Do, so concurrent stale hits still produce only one Microsoft call
  • Cleanup logging — only logs when entries are actually evicted

Set up

Add METRICS_SECRET=<strong-value> in Coolify before deploying, then check via:

https://api.msdl.tech-latest.com/metrics?secret=YOUR_SECRET

Example response

{
    "sku":  { "requests": 8, "cache_hits": 6, "ms_fetches": 2, "neg_hits": 0, "hit_rate": "75.0%", "cache_size": 2 },
    "link": { "requests": 3, "cache_hits": 2, "ms_fetches": 1, "neg_hits": 0, "stale": 0, "hit_rate": "66.7%", "cache_size": 1 },
    "eval": { "requests": 3, "cache_hits": 2, "stale": 0, "hit_rate": "66.7%", "cache_size": 5 },
    "neg_cache_size": 0,
    "total_ms_fetches": 3
}

Test plan

  • Add METRICS_SECRET in Coolify env vars
  • Deploy and confirm /health passes
  • Hit GET /metrics?secret=YOUR_SECRET → should return JSON with live counters
  • Hit /metrics with no secret → 403, wrong secret → 401
  • Watch Coolify logs — cache cleanup: evicted ... should only appear when entries expire

Summary by CodeRabbit

  • New Features

    • Added metrics endpoint for backend performance monitoring (protected by METRICS_SECRET).
  • Improvements

    • Enhanced cache behavior with stale-on-failure mechanism to serve cached data if fetch fails.
    • Optimized performance and reduced contention under concurrent load.
  • Chores

    • Implemented automatic cache cleanup to evict expired entries.

Review Change Stack

## Summary - **`/metrics` endpoint** — auth-protected (METRICS_SECRET env var) JSON endpoint exposing per-endpoint request counts, cache hits, MS fetch counts, neg-cache hits, stale serves, hit rates, and live cache sizes - **Cache eviction** — `cleanupCaches()` goroutine runs every 30min, evicts expired SKU/neg entries and link entries past ExpiresAt + 4h stale window — fixes unbounded map growth - **Local rand source** — package-level `rand.Rand` replaces global rand to avoid mutex contention at high QPS - **Clearer singleflight keys** — `sfKey` separated from `negKey` in both handlers - **Background refresh via singleflight** — stale serve triggers a background goroutine that goes through `sfGroup.Do`, so concurrent stale hits still produce only one Microsoft call - **Cleanup logging** — only logs when entries are actually evicted ## Set up Add `METRICS_SECRET=<strong-value>` in Coolify before deploying, then check via: ``` https://api.msdl.tech-latest.com/metrics?secret=YOUR_SECRET ``` ## Example response ```json { "sku": { "requests": 8, "cache_hits": 6, "ms_fetches": 2, "neg_hits": 0, "hit_rate": "75.0%", "cache_size": 2 }, "link": { "requests": 3, "cache_hits": 2, "ms_fetches": 1, "neg_hits": 0, "stale": 0, "hit_rate": "66.7%", "cache_size": 1 }, "eval": { "requests": 3, "cache_hits": 2, "stale": 0, "hit_rate": "66.7%", "cache_size": 5 }, "neg_cache_size": 0, "total_ms_fetches": 3 } ``` ## Test plan - [ ] Add `METRICS_SECRET` in Coolify env vars - [ ] Deploy and confirm `/health` passes - [ ] Hit `GET /metrics?secret=YOUR_SECRET` → should return JSON with live counters - [ ] Hit `/metrics` with no secret → 403, wrong secret → 401 - [ ] Watch Coolify logs — `cache cleanup: evicted ...` should only appear when entries expire <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added metrics endpoint for backend performance monitoring (protected by METRICS_SECRET). * **Improvements** * Enhanced cache behavior with stale-on-failure mechanism to serve cached data if fetch fails. * Optimized performance and reduced contention under concurrent load. * **Chores** * Implemented automatic cache cleanup to evict expired entries. <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/starkSV/windows-iso-downloader/pull/16?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
coderabbitai[bot] commented 2026-05-18 23:14:57 +05:30 (Migrated from github.com)
📝 Walkthrough

Walkthrough

This PR adds internal observability and improves cache resilience to the backend HTTP service. It introduces package-level metrics counters across /skuinfo, /proxy, and /evallinks endpoints; exposes metrics via a new /metrics endpoint; implements periodic cache cleanup; and enhances /proxy stale-on-failure handling with background refresh. The /skuinfo singleflight key is refactored from negative-cache key to a dedicated sku:<product_id> pattern, and global rand is replaced with a package-level instance for concurrent safety in jitter generation.

Changes

Metrics and Cache Resilience

Layer / File(s) Summary
RNG and metric counter infrastructure
backend/main.go (lines 19–29, 98–119, 164)
Package-level rand.Rand instance seeded at startup; atomic counter variables for request volume, cache hits, negative-cache hits, fetches, and stale serves across three endpoints; jitter function updated to use package-level RNG.
Metrics endpoint handler and cache eviction
backend/main.go (lines 945–1070)
cleanupCaches() function periodically evicts expired SKU, link, and negative-cache entries; /metrics handler authenticates via METRICS_SECRET, reports atomic counter values, computes hit rates, and exposes cache sizes in Prometheus-like plaintext format.
SKU info endpoint monitoring and singleflight optimization
backend/main.go (lines 699–730, 765)
Negative-cache-hit, cache-hit, and fetch metrics integrated; singleflight deduplication key changed from negative-cache key to dedicated sku:<product_id> key with result struct wrapper.
Link proxy endpoint monitoring and stale-on-failure background refresh
backend/main.go (lines 790, 800, 812–847, 881)
Request, cache-hit, negative-cache-hit, fetch, and stale-serve metrics added; stale-on-failure flow reworked to serve stale cache immediately, increment stale metric, and trigger deduped background refresh that re-fetches, recalculates TTL, and updates link cache.
Eval links endpoint monitoring
backend/main.go (lines 902–908)
Request and cache-hit metrics integrated for /evallinks endpoint.
Main function integration
backend/main.go (lines 1089–1097)
Cache cleanup coroutine and /metrics route registered in main() alongside existing session cleanup and eval cache warmup.

Poem

🐰 A rabbit hops through caches bright,
Metrics gathered, stale served right,
Background refreshes deduplicate the load,
Observability lights the Kubernetes road! 🚀

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | 4 | 1

Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed checks (4 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely summarizes the main changes: adding a /metrics endpoint, implementing cache eviction, and improving observability. All three components align with the actual changeset.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/metrics

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Comment @coderabbitai help to get the list of available commands and usage tips.

<!-- This is an auto-generated comment: summarize by coderabbit.ai --> <!-- walkthrough_start --> <details> <summary>📝 Walkthrough</summary> ## Walkthrough This PR adds internal observability and improves cache resilience to the backend HTTP service. It introduces package-level metrics counters across `/skuinfo`, `/proxy`, and `/evallinks` endpoints; exposes metrics via a new `/metrics` endpoint; implements periodic cache cleanup; and enhances `/proxy` stale-on-failure handling with background refresh. The `/skuinfo` singleflight key is refactored from negative-cache key to a dedicated `sku:<product_id>` pattern, and global `rand` is replaced with a package-level instance for concurrent safety in jitter generation. ## Changes **Metrics and Cache Resilience** |Layer / File(s)|Summary| |---|---| |**RNG and metric counter infrastructure** <br> `backend/main.go` (lines 19–29, 98–119, 164)|Package-level `rand.Rand` instance seeded at startup; atomic counter variables for request volume, cache hits, negative-cache hits, fetches, and stale serves across three endpoints; jitter function updated to use package-level RNG.| |**Metrics endpoint handler and cache eviction** <br> `backend/main.go` (lines 945–1070)|`cleanupCaches()` function periodically evicts expired SKU, link, and negative-cache entries; `/metrics` handler authenticates via `METRICS_SECRET`, reports atomic counter values, computes hit rates, and exposes cache sizes in Prometheus-like plaintext format.| |**SKU info endpoint monitoring and singleflight optimization** <br> `backend/main.go` (lines 699–730, 765)|Negative-cache-hit, cache-hit, and fetch metrics integrated; singleflight deduplication key changed from negative-cache key to dedicated `sku:<product_id>` key with result struct wrapper.| |**Link proxy endpoint monitoring and stale-on-failure background refresh** <br> `backend/main.go` (lines 790, 800, 812–847, 881)|Request, cache-hit, negative-cache-hit, fetch, and stale-serve metrics added; stale-on-failure flow reworked to serve stale cache immediately, increment stale metric, and trigger deduped background refresh that re-fetches, recalculates TTL, and updates link cache.| |**Eval links endpoint monitoring** <br> `backend/main.go` (lines 902–908)|Request and cache-hit metrics integrated for `/evallinks` endpoint.| |**Main function integration** <br> `backend/main.go` (lines 1089–1097)|Cache cleanup coroutine and `/metrics` route registered in `main()` alongside existing session cleanup and eval cache warmup.| ## Poem > 🐰 *A rabbit hops through caches bright,* > *Metrics gathered, stale served right,* > *Background refreshes deduplicate the load,* > *Observability lights the Kubernetes road!* 🚀 🎯 3 (Moderate) | ⏱️ ~25 minutes </details> <!-- walkthrough_end --> <!-- pre_merge_checks_walkthrough_start --> <details> <summary>🚥 Pre-merge checks | ✅ 4 | ❌ 1</summary> ### ❌ Failed checks (1 warning) | Check name | Status | Explanation | Resolution | | :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- | | Docstring Coverage | ⚠️ Warning | Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. | <details> <summary>✅ Passed checks (4 passed)</summary> | Check name | Status | Explanation | | :------------------------: | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. | | Title check | ✅ Passed | The title clearly and concisely summarizes the main changes: adding a /metrics endpoint, implementing cache eviction, and improving observability. All three components align with the actual changeset. | | Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. | | Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. | </details> <sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub> </details> <!-- pre_merge_checks_walkthrough_end --> <!-- finishing_touch_checkbox_start --> <details> <summary>✨ Finishing Touches</summary> <details> <summary>📝 Generate docstrings</summary> - [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> Create stacked PR - [ ] <!-- {"checkboxId": "3e1879ae-f29b-4d0d-8e06-d12b7ba33d98"} --> Commit on current branch </details> <details> <summary>🧪 Generate unit tests (beta)</summary> - [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Create PR with unit tests - [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Commit unit tests in branch `feat/metrics` </details> </details> <!-- finishing_touch_checkbox_end --> <!-- This is an auto-generated comment: all tool run failures by coderabbit.ai --> > [!WARNING] > There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. > > <details> > <summary>🔧 golangci-lint (2.12.2)</summary> > > level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" > > > > > </details> <!-- end of auto-generated comment: all tool run failures by coderabbit.ai --> <!-- tips_start --> --- <sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub> <!-- tips_end --> <!-- internal state start --> <!-- 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 --> <!-- internal state end -->
coderabbitai[bot] (Migrated from github.com) reviewed 2026-05-18 23:17:59 +05:30
coderabbitai[bot] (Migrated from github.com) left a comment

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/main.go`:
- Around line 831-847: The background refresh inside the goroutine (the
sfGroup.Do closure that calls fetchDownloadLinksFromMS) updates linkCache on
success but never increments the mLinkFetches metric; after a successful fetch
and cache update (i.e., after linkCache[cacheKey] = ... and before returning
nil), increment the mLinkFetches counter the same way you do for foreground
fetches (e.g., atomic.AddInt64(&mLinkFetches, 1) or the existing metric helper)
so /metrics reflects these background Microsoft API calls.
- Around line 26-28: The package-level rng (var rng = rand.New(...)) is being
used concurrently by jitter(), causing a data race because rand.Rand is not
goroutine-safe; either guard rng with a sync.Mutex (add a mutex variable and
lock/unlock around all accesses to rng inside jitter() and any other uses) or
replace uses of rng in jitter() with the global, thread-safe rand functions
(e.g., rand.Int63n) and remove the package-level rng; update references to
rng/jitter() accordingly to eliminate unsynchronized access.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 659481bd-0af9-4bb5-b443-92890b7ad5c3

📥 Commits

Reviewing files that changed from the base of the PR and between 3329cd1b52 and 90c498233b.

📒 Files selected for processing (1)
  • backend/main.go
**Actionable comments posted: 2** <details> <summary>🤖 Prompt for all review comments with AI agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. Inline comments: In `@backend/main.go`: - Around line 831-847: The background refresh inside the goroutine (the sfGroup.Do closure that calls fetchDownloadLinksFromMS) updates linkCache on success but never increments the mLinkFetches metric; after a successful fetch and cache update (i.e., after linkCache[cacheKey] = ... and before returning nil), increment the mLinkFetches counter the same way you do for foreground fetches (e.g., atomic.AddInt64(&mLinkFetches, 1) or the existing metric helper) so /metrics reflects these background Microsoft API calls. - Around line 26-28: The package-level rng (var rng = rand.New(...)) is being used concurrently by jitter(), causing a data race because rand.Rand is not goroutine-safe; either guard rng with a sync.Mutex (add a mutex variable and lock/unlock around all accesses to rng inside jitter() and any other uses) or replace uses of rng in jitter() with the global, thread-safe rand functions (e.g., rand.Int63n) and remove the package-level rng; update references to rng/jitter() accordingly to eliminate unsynchronized access. ``` </details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro Plus **Run ID**: `659481bd-0af9-4bb5-b443-92890b7ad5c3` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 3329cd1b52ac5dc7c2ed625b715fafc2955798f6 and 90c498233bc85602e282ff5d3a44b66f82b16a3a. </details> <details> <summary>📒 Files selected for processing (1)</summary> * `backend/main.go` </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
@ -24,1 +25,4 @@
// Package-level rand source — avoids global mutex contention under concurrent load.
// Go 1.20+ auto-seeds the global rand, but a local source is faster at high QPS.
var rng = rand.New(rand.NewSource(time.Now().UnixNano()))
coderabbitai[bot] (Migrated from github.com) commented 2026-05-18 23:17:58 +05:30

⚠️ Potential issue | 🔴 Critical | Quick win

🧩 Analysis chain

🌐 Web query:

Is Go rand.Rand safe for concurrent use?

💡 Result:

No, instances of rand.Rand are not safe for concurrent use [1][2]. According to the official Go documentation, both the rand.Rand type and the underlying rand.Source type must be used by only a single goroutine at a time [1][2]. If you need to share a random number generator across multiple goroutines, you must implement your own synchronization, such as using a sync.Mutex to guard access to the rand.Rand instance [3][4]. In contrast, the top-level functions provided by the math/rand package (such as rand.Float64 and rand.Int) are safe for concurrent use because they utilize a global, synchronized source [1][5]. If you require a cryptographically secure random number generator that is safe for concurrent use, you should use the crypto/rand package, which provides a global, thread-safe reader [6].

Citations:


🏁 Script executed:

# Check the jitter() function definition and usage
grep -n "func jitter" backend/main.go

Repository: starkSV/windows-iso-downloader

Length of output: 132


🏁 Script executed:

# Check all occurrences of rng variable
grep -n "rng" backend/main.go

Repository: starkSV/windows-iso-downloader

Length of output: 198


🏁 Script executed:

# Check concurrent handler/goroutine contexts
grep -n "go func\|func.*Handler\|handleRequest" backend/main.go | head -20

Repository: starkSV/windows-iso-downloader

Length of output: 225


🏁 Script executed:

# Show the jitter function in full
sed -n '163,167p' backend/main.go

Repository: starkSV/windows-iso-downloader

Length of output: 212


🏁 Script executed:

# Find all calls to jitter()
grep -n "jitter(" backend/main.go

Repository: starkSV/windows-iso-downloader

Length of output: 321


🏁 Script executed:

# Check if sync.Mutex is declared for rng protection
sed -n '20,35p' backend/main.go

Repository: starkSV/windows-iso-downloader

Length of output: 648


Fix data race: rand.Rand is not safe for concurrent use.

The jitter() function at lines 163–166 accesses an unsynchronized rand.Rand instance from concurrent HTTP handlers (lines 174, 209, 761), creating a data race. According to Go documentation, rand.Rand must be accessed by only a single goroutine at a time.

Add sync.Mutex protection:

Fix: Add mutex protection to rng
 var rng = rand.New(rand.NewSource(time.Now().UnixNano()))
+var rngMu sync.Mutex
 
 func jitter(base, maxJ time.Duration) time.Duration {
+	rngMu.Lock()
 	offset := time.Duration(rng.Int63n(int64(maxJ*2))) - maxJ
+	rngMu.Unlock()
 	return base + offset
 }

Alternatively, use the global rand functions (e.g., rand.Int63n()), which are mutex-protected by the runtime, though contention is minimal at typical QPS.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.go` around lines 26 - 28, The package-level rng (var rng =
rand.New(...)) is being used concurrently by jitter(), causing a data race
because rand.Rand is not goroutine-safe; either guard rng with a sync.Mutex (add
a mutex variable and lock/unlock around all accesses to rng inside jitter() and
any other uses) or replace uses of rng in jitter() with the global, thread-safe
rand functions (e.g., rand.Int63n) and remove the package-level rng; update
references to rng/jitter() accordingly to eliminate unsynchronized access.
_⚠️ Potential issue_ | _🔴 Critical_ | _⚡ Quick win_ <details> <summary>🧩 Analysis chain</summary> 🌐 Web query: `Is Go rand.Rand safe for concurrent use?` 💡 Result: No, instances of rand.Rand are not safe for concurrent use [1][2]. According to the official Go documentation, both the rand.Rand type and the underlying rand.Source type must be used by only a single goroutine at a time [1][2]. If you need to share a random number generator across multiple goroutines, you must implement your own synchronization, such as using a sync.Mutex to guard access to the rand.Rand instance [3][4]. In contrast, the top-level functions provided by the math/rand package (such as rand.Float64 and rand.Int) are safe for concurrent use because they utilize a global, synchronized source [1][5]. If you require a cryptographically secure random number generator that is safe for concurrent use, you should use the crypto/rand package, which provides a global, thread-safe reader [6]. Citations: - 1: https://pkg.go.dev/math/rand - 2: https://pkg.go.dev/math/rand/v2@go1.25.5 - 3: https://github.com/golang/go/issues/3611 - 4: https://stackoverflow.com/questions/48958886/how-to-create-a-thread-safe-rand-source - 5: https://go.dev/src/math/rand/rand.go - 6: https://stackoverflow.com/questions/75685374/is-golang-crypto-rand-thread-safe --- 🏁 Script executed: ```shell # Check the jitter() function definition and usage grep -n "func jitter" backend/main.go ``` Repository: starkSV/windows-iso-downloader Length of output: 132 --- 🏁 Script executed: ```shell # Check all occurrences of rng variable grep -n "rng" backend/main.go ``` Repository: starkSV/windows-iso-downloader Length of output: 198 --- 🏁 Script executed: ```shell # Check concurrent handler/goroutine contexts grep -n "go func\|func.*Handler\|handleRequest" backend/main.go | head -20 ``` Repository: starkSV/windows-iso-downloader Length of output: 225 --- 🏁 Script executed: ```shell # Show the jitter function in full sed -n '163,167p' backend/main.go ``` Repository: starkSV/windows-iso-downloader Length of output: 212 --- 🏁 Script executed: ```shell # Find all calls to jitter() grep -n "jitter(" backend/main.go ``` Repository: starkSV/windows-iso-downloader Length of output: 321 --- 🏁 Script executed: ```shell # Check if sync.Mutex is declared for rng protection sed -n '20,35p' backend/main.go ``` Repository: starkSV/windows-iso-downloader Length of output: 648 --- </details> **Fix data race: `rand.Rand` is not safe for concurrent use.** The `jitter()` function at lines 163–166 accesses an unsynchronized `rand.Rand` instance from concurrent HTTP handlers (lines 174, 209, 761), creating a data race. According to Go documentation, `rand.Rand` must be accessed by only a single goroutine at a time. Add `sync.Mutex` protection: <details> <summary>Fix: Add mutex protection to rng</summary> ```diff var rng = rand.New(rand.NewSource(time.Now().UnixNano())) +var rngMu sync.Mutex func jitter(base, maxJ time.Duration) time.Duration { + rngMu.Lock() offset := time.Duration(rng.Int63n(int64(maxJ*2))) - maxJ + rngMu.Unlock() return base + offset } ``` </details> Alternatively, use the global `rand` functions (e.g., `rand.Int63n()`), which are mutex-protected by the runtime, though contention is minimal at typical QPS. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/main.go` around lines 26 - 28, The package-level rng (var rng = rand.New(...)) is being used concurrently by jitter(), causing a data race because rand.Rand is not goroutine-safe; either guard rng with a sync.Mutex (add a mutex variable and lock/unlock around all accesses to rng inside jitter() and any other uses) or replace uses of rng in jitter() with the global, thread-safe rand functions (e.g., rand.Int63n) and remove the package-level rng; update references to rng/jitter() accordingly to eliminate unsynchronized access. ``` </details> <!-- fingerprinting:phantom:medusa:ocelot --> <!-- This is an auto-generated comment by CodeRabbit -->
@ -885,0 +962,4 @@
// Keep link entries for 4h past soft expiry — stale-on-failure window
linkCacheMu.Lock()
for k, v := range linkCache {
if now.After(v.ExpiresAt.Add(4 * time.Hour)) {
coderabbitai[bot] (Migrated from github.com) commented 2026-05-18 23:17:58 +05:30

⚠️ Potential issue | 🟡 Minor | Quick win

Background refresh does not increment mLinkFetches on success.

When the background refresh successfully fetches from Microsoft (line 835), it updates the cache but doesn't increment mLinkFetches. This means the /metrics endpoint will undercount actual Microsoft API calls when stale-on-failure triggers background refreshes.

📊 Proposed fix
 				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()
+					atomic.AddInt64(&mLinkFetches, 1)
 					log.Printf("/proxy: background refresh succeeded for %s:%s, cached until %s\n", productID, skuID, exp.Format(time.RFC3339))
 					return nil, nil
 				})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/main.go` around lines 831 - 847, The background refresh inside the
goroutine (the sfGroup.Do closure that calls fetchDownloadLinksFromMS) updates
linkCache on success but never increments the mLinkFetches metric; after a
successful fetch and cache update (i.e., after linkCache[cacheKey] = ... and
before returning nil), increment the mLinkFetches counter the same way you do
for foreground fetches (e.g., atomic.AddInt64(&mLinkFetches, 1) or the existing
metric helper) so /metrics reflects these background Microsoft API calls.
_⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_ **Background refresh does not increment `mLinkFetches` on success.** When the background refresh successfully fetches from Microsoft (line 835), it updates the cache but doesn't increment `mLinkFetches`. This means the `/metrics` endpoint will undercount actual Microsoft API calls when stale-on-failure triggers background refreshes. <details> <summary>📊 Proposed fix</summary> ```diff 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() + atomic.AddInt64(&mLinkFetches, 1) log.Printf("/proxy: background refresh succeeded for %s:%s, cached until %s\n", productID, skuID, exp.Format(time.RFC3339)) return nil, nil }) ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/main.go` around lines 831 - 847, The background refresh inside the goroutine (the sfGroup.Do closure that calls fetchDownloadLinksFromMS) updates linkCache on success but never increments the mLinkFetches metric; after a successful fetch and cache update (i.e., after linkCache[cacheKey] = ... and before returning nil), increment the mLinkFetches counter the same way you do for foreground fetches (e.g., atomic.AddInt64(&mLinkFetches, 1) or the existing metric helper) so /metrics reflects these background Microsoft API calls. ``` </details> <!-- fingerprinting:phantom:medusa:ocelot --> <!-- This is an auto-generated comment by CodeRabbit -->
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: shekhar/windows-iso-downloader#16
No description provided.