mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-02 16:20:57 +02:00
perf(extensions): refresh provider health off the download path
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user