perf(extensions): refresh provider health off the download path

This commit is contained in:
zarzet
2026-08-30 23:18:12 +07:00
parent 91b2de8175
commit 0edd0f8f04
4 changed files with 149 additions and 12 deletions
+15 -1
View File
@@ -381,7 +381,21 @@ func fallbackRuntimeHealthStatus(ext *loadedExtension) string {
return "unknown"
}
status := strings.ToLower(strings.TrimSpace(CheckExtensionHealthCached(ext).Status))
health, cached := PeekExtensionHealthCached(ext)
if !cached {
staleHealth, hasStale := peekExtensionHealthStale(ext)
RefreshExtensionHealthAsync(ext)
if !hasStale {
return "unknown"
}
// An expired offline verdict must not keep suppressing a recovered
// provider while its refresh runs in the background.
if strings.EqualFold(staleHealth.Status, "offline") {
return "unknown"
}
health = staleHealth
}
status := strings.ToLower(strings.TrimSpace(health.Status))
switch status {
case "online", "degraded", "offline":
return status
+88 -11
View File
@@ -48,16 +48,89 @@ type cachedExtensionHealthResult struct {
}
var (
extensionHealthCacheMu sync.Mutex
extensionHealthCache = map[string]cachedExtensionHealthResult{}
extensionHealthCacheMu sync.Mutex
extensionHealthCache = map[string]cachedExtensionHealthResult{}
extensionHealthRefresh = map[string]struct{}{}
extensionHealthGeneration uint64
)
func clearExtensionHealthCache() {
extensionHealthCacheMu.Lock()
extensionHealthCache = map[string]cachedExtensionHealthResult{}
extensionHealthRefresh = map[string]struct{}{}
extensionHealthGeneration++
extensionHealthCacheMu.Unlock()
}
// PeekExtensionHealthCached returns only an already-computed, unexpired
// health snapshot. It never performs network I/O. Download provider ordering
// uses this path so an informational health endpoint can never delay the real
// provider attempt.
func PeekExtensionHealthCached(ext *loadedExtension) (ExtensionHealthResult, bool) {
if ext == nil || ext.Manifest == nil || len(ext.Manifest.ServiceHealth) == 0 {
return ExtensionHealthResult{}, false
}
cacheKey := strings.TrimSpace(ext.ID)
if cacheKey == "" {
return ExtensionHealthResult{}, false
}
now := time.Now()
extensionHealthCacheMu.Lock()
defer extensionHealthCacheMu.Unlock()
cached, ok := extensionHealthCache[cacheKey]
if !ok || !now.Before(cached.expiresAt) {
return ExtensionHealthResult{}, false
}
return cached.result, true
}
func peekExtensionHealthStale(ext *loadedExtension) (ExtensionHealthResult, bool) {
if ext == nil {
return ExtensionHealthResult{}, false
}
cacheKey := strings.TrimSpace(ext.ID)
if cacheKey == "" {
return ExtensionHealthResult{}, false
}
extensionHealthCacheMu.Lock()
defer extensionHealthCacheMu.Unlock()
cached, ok := extensionHealthCache[cacheKey]
return cached.result, ok
}
// RefreshExtensionHealthAsync coalesces background refreshes per extension.
// Callers deliberately do not wait: stale or missing health data must not be
// on the latency-critical download path.
func RefreshExtensionHealthAsync(ext *loadedExtension) {
if ext == nil || ext.Manifest == nil || len(ext.Manifest.ServiceHealth) == 0 {
return
}
cacheKey := strings.TrimSpace(ext.ID)
if cacheKey == "" {
return
}
extensionHealthCacheMu.Lock()
if _, refreshing := extensionHealthRefresh[cacheKey]; refreshing {
extensionHealthCacheMu.Unlock()
return
}
extensionHealthRefresh[cacheKey] = struct{}{}
generation := extensionHealthGeneration
extensionHealthCacheMu.Unlock()
go func() {
result := CheckExtensionHealth(ext)
extensionHealthCacheMu.Lock()
if generation == extensionHealthGeneration {
cacheExtensionHealthResultLocked(ext, result)
delete(extensionHealthRefresh, cacheKey)
}
extensionHealthCacheMu.Unlock()
}()
}
func CheckExtensionHealthJSON(extensionID string) (string, error) {
manager := getExtensionManager()
ext, err := manager.GetExtension(extensionID)
@@ -84,14 +157,9 @@ func CheckExtensionHealthCached(ext *loadedExtension) ExtensionHealthResult {
return CheckExtensionHealth(ext)
}
now := time.Now()
extensionHealthCacheMu.Lock()
cached, ok := extensionHealthCache[cacheKey]
if ok && now.Before(cached.expiresAt) {
extensionHealthCacheMu.Unlock()
return cached.result
if cached, ok := PeekExtensionHealthCached(ext); ok {
return cached
}
extensionHealthCacheMu.Unlock()
result := CheckExtensionHealth(ext)
cacheExtensionHealthResult(ext, result)
@@ -108,17 +176,26 @@ func cacheExtensionHealthResult(ext *loadedExtension, result ExtensionHealthResu
return
}
extensionHealthCacheMu.Lock()
cacheExtensionHealthResultLocked(ext, result)
extensionHealthCacheMu.Unlock()
}
// cacheExtensionHealthResultLocked stores a result while the caller owns
// extensionHealthCacheMu. Keeping this small helper avoids a clear-vs-refresh
// race without taking the same mutex recursively.
func cacheExtensionHealthResultLocked(ext *loadedExtension, result ExtensionHealthResult) {
cacheKey := strings.TrimSpace(ext.ID)
ttl := extensionHealthCacheTTL(ext.Manifest.ServiceHealth)
if result.Status == "unknown" && ttl > extensionHealthUnknownCache {
ttl = extensionHealthUnknownCache
}
extensionHealthCacheMu.Lock()
extensionHealthCache[cacheKey] = cachedExtensionHealthResult{
result: result,
expiresAt: time.Now().Add(ttl),
}
extensionHealthCacheMu.Unlock()
}
func CheckExtensionHealth(ext *loadedExtension) ExtensionHealthResult {
@@ -6,6 +6,7 @@ import (
"net"
"strings"
"testing"
"time"
)
func TestExtensionHealthClassificationAndValidation(t *testing.T) {
@@ -74,3 +75,39 @@ func TestCoverHelpersRejectEmptyURL(t *testing.T) {
t.Fatalf("expected empty cover error")
}
}
func TestPeekExtensionHealthCachedNeverRefreshesSynchronously(t *testing.T) {
clearExtensionHealthCache()
ext := &loadedExtension{
ID: "cached-health-ext",
Manifest: &ExtensionManifest{ServiceHealth: []ExtensionHealthCheck{{
ID: "main", URL: "https://status.example.com",
}}},
}
if _, ok := PeekExtensionHealthCached(ext); ok {
t.Fatal("unexpected cache hit")
}
want := ExtensionHealthResult{ExtensionID: ext.ID, Status: "degraded"}
extensionHealthCacheMu.Lock()
extensionHealthCache[ext.ID] = cachedExtensionHealthResult{
result: want, expiresAt: time.Now().Add(time.Minute),
}
extensionHealthCacheMu.Unlock()
got, ok := PeekExtensionHealthCached(ext)
if !ok || got.Status != want.Status {
t.Fatalf("cached health = %#v/%v", got, ok)
}
extensionHealthCacheMu.Lock()
entry := extensionHealthCache[ext.ID]
entry.expiresAt = time.Now().Add(-time.Second)
extensionHealthCache[ext.ID] = entry
extensionHealthCacheMu.Unlock()
if _, ok := PeekExtensionHealthCached(ext); ok {
t.Fatal("expired cache entry was returned")
}
if stale, ok := peekExtensionHealthStale(ext); !ok || stale.Status != want.Status {
t.Fatalf("stale health snapshot = %#v/%v", stale, ok)
}
}
+9
View File
@@ -182,11 +182,20 @@ func TestPrioritizeFallbackProvidersByHealthPrefersOnlineAndSkipsOffline(t *test
manager.mu.Unlock()
extensionHealthCacheMu.Lock()
delete(extensionHealthCache, amazon.ID)
delete(extensionHealthCache, deezer.ID)
extensionHealthCacheMu.Unlock()
}()
extensionHealthCacheMu.Lock()
extensionHealthCache[amazon.ID] = cachedExtensionHealthResult{
result: ExtensionHealthResult{
ExtensionID: amazon.ID,
Status: "offline",
CheckedAt: time.Now().UTC().Format(time.RFC3339),
},
expiresAt: time.Now().Add(time.Minute),
}
extensionHealthCache[deezer.ID] = cachedExtensionHealthResult{
result: ExtensionHealthResult{
ExtensionID: deezer.ID,