mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-29 15:28:48 +02:00
Stability: - transient timeouts no longer classified as ISP blocking; they fall through to retry backoff (hard blocks - DNS/RST/cert - still abort) - connectivity change now closes idle Go connections in every network mode (debounced), so pooled sockets from the old interface are not reused after a wifi/cellular handoff - download body reads get a 60s stall watchdog that cancels and surfaces a retryable network error instead of hanging; distinct from user cancellation - ResponseHeaderTimeout 45s on all transports; IdleConnTimeout 90->60s; dial timeout 30->10s; retry backoff gains full jitter; Retry-After honored on 5xx and Deezer 429 Data usage / speed: - cover downloads deduplicated with singleflight plus a 24MB/15min in-memory cache keyed by final URL (album batches fetched the same 1800px cover once per track before) - song.link availability cached (30min positive / 5min negative) in front of the 9-req/min rate limiter - uTLS Cloudflare path now pools one HTTP/2 connection per host with a shared TLS session cache instead of a full handshake per request Deezer artist track-count N+1 kept: counts feed the discography download UI and are already amortized by the artist TTL cache.
97 lines
2.1 KiB
Go
97 lines
2.1 KiB
Go
package gobackend
|
|
|
|
import (
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func resetCoverCache() {
|
|
coverMu.Lock()
|
|
coverCache = map[string]*coverCacheEntry{}
|
|
coverInflight = map[string]*coverInflightCall{}
|
|
coverCacheBytes = 0
|
|
coverMu.Unlock()
|
|
}
|
|
|
|
func TestFetchCoverCachedSingleflight(t *testing.T) {
|
|
orig := coverFetch
|
|
defer func() { coverFetch = orig }()
|
|
resetCoverCache()
|
|
|
|
var calls int32
|
|
entered := make(chan struct{})
|
|
release := make(chan struct{})
|
|
coverFetch = func(string) ([]byte, error) {
|
|
if atomic.AddInt32(&calls, 1) == 1 {
|
|
close(entered)
|
|
}
|
|
<-release
|
|
return []byte("coverbytes"), nil
|
|
}
|
|
|
|
const url = "https://cdn.example/cover_max.jpg"
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if _, err := fetchCoverCached(url); err != nil {
|
|
t.Errorf("leader fetch error: %v", err)
|
|
}
|
|
}()
|
|
|
|
<-entered // leader has registered inflight and is blocked in coverFetch
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
if _, err := fetchCoverCached(url); err != nil {
|
|
t.Errorf("follower fetch error: %v", err)
|
|
}
|
|
}()
|
|
|
|
close(release)
|
|
wg.Wait()
|
|
|
|
if got := atomic.LoadInt32(&calls); got != 1 {
|
|
t.Fatalf("expected 1 fetch for concurrent requests, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestFetchCoverCachedTTLExpiry(t *testing.T) {
|
|
orig := coverFetch
|
|
defer func() { coverFetch = orig }()
|
|
resetCoverCache()
|
|
|
|
var calls int32
|
|
coverFetch = func(string) ([]byte, error) {
|
|
atomic.AddInt32(&calls, 1)
|
|
return []byte("data"), nil
|
|
}
|
|
|
|
const url = "https://cdn.example/ttl.jpg"
|
|
if _, err := fetchCoverCached(url); err != nil {
|
|
t.Fatalf("first fetch error: %v", err)
|
|
}
|
|
// second call served from cache
|
|
if _, err := fetchCoverCached(url); err != nil {
|
|
t.Fatalf("second fetch error: %v", err)
|
|
}
|
|
if got := atomic.LoadInt32(&calls); got != 1 {
|
|
t.Fatalf("expected cache hit, got %d fetches", got)
|
|
}
|
|
|
|
// expire the entry and confirm a refetch
|
|
coverMu.Lock()
|
|
coverCache[url].expiresAt = time.Now().Add(-time.Minute)
|
|
coverMu.Unlock()
|
|
|
|
if _, err := fetchCoverCached(url); err != nil {
|
|
t.Fatalf("third fetch error: %v", err)
|
|
}
|
|
if got := atomic.LoadInt32(&calls); got != 2 {
|
|
t.Fatalf("expected refetch after TTL expiry, got %d fetches", got)
|
|
}
|
|
}
|