Frontend Asset Optimization

grafana/grafana#90939
Golden recall: 50% Extra findings: 1

Golden Comments (1/2 found)

# Comment Severity Status FriendlyReviewer Detail
#1 The GetWebAssets function implements an incomplete double-checked locking pattern for caching web assets. The function first checks if the cache is populated using a read lock (RLock), and if the cache is empty, it acquires a write lock to populate it. However, it fails to re-check whether the cache was populated by another goroutine while waiting to acquire the write lock MEDIUM ✓ Found "After acquiring the write lock, the code unconditionally re-reads/re-parses the manifest without re-checking whether another goroutine already populated the cache between RUnlock and Lock. Under concurrent cold start, multiple callers redundantly parse and overwrite the cache. Adding a double-checked read inside the critical section would avoid redundant work and further reduce lock hold time" (webassets.go L59)
#2 In addition to the missing double-check, the function has a critical flaw in its error handling: it unconditionally assigns the fetch result to the cache (line 69: entryPointAssetsCache = result) regardless of whether the fetch succeeded or failed. When an error occurs during asset fetching, result is nil, and this nil value overwrites any previously valid cache entry HIGH ✗ Not found The review for this PR is short — 2 low-severity findings (lock scope at L48, missing double-check at L59) — and validates the race fix as correct. The unconditional assignment of the fetch result on error is never mentioned: when the fetch fails, result is nil and overwrites any previously valid cache entry. Probable location: pkg/api/webassets/webassets.go:69

Supplementary Findings (1 finding)

Finding File Severity Legitimate?
Exclusive lock held across slow disk/network I/O (CDN fetch, file read/parse) serializes concurrent fast-path readers during cold start and Dev-mode callers webassets.go:48 LOW ✓ Real lock-contention issue on the cold-start path, but purely a performance optimization — the review itself confirms the race fix is correct

Honest Analysis

FriendlyReviewer found 1 of the 2 golden bugs of this PR — recall 50%. The golden on the incomplete double-checked locking pattern is matched directly: the review identifies that after acquiring the write lock, the manifest is unconditionally re-read without re-checking whether another goroutine already populated the cache between RUnlock and Lock, causing redundant parse and overwrite work under concurrent cold start. The dataset rates this golden MEDIUM, and FriendlyReviewer flags it as a low-severity optimization — the severity read is consistent.

The missed golden is the more damaging issue. On a fetch error, the function unconditionally assigns the nil result to the cache (line 69), destroying any valid entry already in the cache. The review's two findings are both low-severity and both frame the fix as correct — "no remaining races, deadlocks, or behavioral changes" — so the error path never received scrutiny. Given that a failed CDN fetch or file read is an ordinary occurrence, a nil cache is a plausible steady-state outcome. This is a genuine blind spot, and it happens to be the golden the dataset rates HIGH.

The single supplementary finding (lock scope, L48) is legitimate: holding the exclusive lock across slow disk/network I/O serializes all concurrent fast-path readers during cold start, and building the result outside the lock is sound advice. It is a performance optimization only, and the review correctly frames it as non-blocking. Overall this is an average result: the correctness verification is thorough for what it covers, but the review is short (2 findings) and does not match the reference on the error path.

FriendlyReviewer's full review

As posted on the PR — the complete output from FriendlyReviewer.

friendly-reviewer bot commented now

Hi there 👋

🌤️ Tech ☀️ Feat
2 low 2 ok

The data race fix in pkg/api/webassets/webassets.go is correct: all accesses to the shared global cache are now properly guarded by a mutex, with no remaining races, deadlocks, or behavioral changes. Two non-blocking optimizations are suggested regarding lock scope: (1) the exclusive lock is held across potentially slow disk/network I/O, which can serialize concurrent callers during cold start or in Dev mode; (2) the code unconditionally re-reads/re-parses the manifest after acquiring the write lock, causing redundant work under concurrent cold start. Neither issue is a correctness bug, but both are worth addressing for consistency with standard double-checked locking patterns.


Small things (take or leave)
  • 🔵 pkg/api/webassets/webassets.go (L48)
    The exclusive lock (entryPointAssetsCacheMu.Lock()) is held while performing potentially slow disk/network I/O (CDN fetch or file read/parse). This blocks all concurrent fast-path readers during cold start and serializes Dev-mode callers. Consider constructing the result outside the lock and only taking the lock to publish the cached value, shrinking the critical section to a pointer store.
  • 🔵 pkg/api/webassets/webassets.go (L59)
    After acquiring the write lock, the code unconditionally re-reads/re-parses the manifest without re-checking whether another goroutine already populated the cache between RUnlock and Lock. Under concurrent cold start, multiple callers redundantly parse and overwrite the cache. Adding a double-checked read inside the critical section would avoid redundant work and further reduce lock hold time.

✅ Feature-level checklist

Looks good

  • Fix the data race when accessing shared global state holding web assets.
    The fix ensures all reads/writes to entryPointAssetsCache are guarded by a mutex; no remaining data race was found.
  • It works as expected from a user's perspective.
    The fix preserves behavior: the same cached pointer is returned when populated, and the manifest is rebuilt otherwise. No user-visible changes introduced.