mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-26 03:20:45 +02:00
perf(network): mobile stability and data-usage fixes from network audit
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.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -276,6 +277,49 @@ func (r *extensionRuntime) bindDownloadCancelContext(req *http.Request) *http.Re
|
||||
return req.WithContext(initDownloadCancel(itemID))
|
||||
}
|
||||
|
||||
// downloadStallTimeout is how long a download may go without receiving a single
|
||||
// byte before the stall watchdog aborts it. A dead radio mid-transfer otherwise
|
||||
// blocks on Body.Read until the 24h client timeout with no error and no retry.
|
||||
const downloadStallTimeout = 60 * time.Second
|
||||
|
||||
// stallWatchdog cancels an in-flight download when no data arrives within
|
||||
// timeout. It wraps the request context in a child cancel so firing it does NOT
|
||||
// set the user-cancel flag (isDownloadCancelled stays false) — a stall is a
|
||||
// distinct, retryable condition. Call reset() after every successful Read and
|
||||
// stop() when the transfer ends.
|
||||
type stallWatchdog struct {
|
||||
cancel context.CancelFunc
|
||||
timer *time.Timer
|
||||
timeout time.Duration
|
||||
stalled atomic.Bool
|
||||
}
|
||||
|
||||
func bindStallWatchdog(req *http.Request, timeout time.Duration) (*http.Request, *stallWatchdog) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
w := &stallWatchdog{cancel: cancel, timeout: timeout}
|
||||
w.timer = time.AfterFunc(timeout, func() {
|
||||
w.stalled.Store(true)
|
||||
cancel()
|
||||
})
|
||||
return req.WithContext(ctx), w
|
||||
}
|
||||
|
||||
func (w *stallWatchdog) reset() { w.timer.Reset(w.timeout) }
|
||||
|
||||
// stop halts the timer and releases the child context so a completed download
|
||||
// leaks neither a pending timer nor a live cancel func.
|
||||
func (w *stallWatchdog) stop() {
|
||||
w.timer.Stop()
|
||||
w.cancel()
|
||||
}
|
||||
|
||||
// stallError is returned when the watchdog fires. The message is deliberately
|
||||
// free of "cancel" and worded to classify as retryable network failure, so the
|
||||
// fallback layer retries instead of treating it as a user cancellation.
|
||||
func (r *extensionRuntime) stallError() goja.Value {
|
||||
return r.jsError("download stalled: no data received for %ds (network timeout)", int(downloadStallTimeout.Seconds()))
|
||||
}
|
||||
|
||||
func newExtensionHTTPClient(ext *loadedExtension, jar http.CookieJar, timeout time.Duration, compressResponses bool) *http.Client {
|
||||
// Extension sandbox enforces HTTPS-only domains. Do not apply global
|
||||
// allow_http scheme downgrade here, because some extension APIs (e.g.
|
||||
|
||||
Reference in New Issue
Block a user