From ef62fb218a46289a9b72204419c85802b17245d1 Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 1 Jan 2026 21:42:25 +0700 Subject: [PATCH] fix: add connection pooling and periodic cleanup to prevent TCP exhaustion --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 6 +++ go_backend/exports.go | 6 +++ go_backend/httputil.go | 49 ++++++++++++++++++- lib/providers/download_queue_provider.dart | 25 ++++++++++ lib/services/platform_bridge.dart | 6 +++ 5 files changed, 91 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 076d74a3..f329285e 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -127,6 +127,12 @@ class MainActivity: FlutterActivity() { } result.success(response) } + "cleanupConnections" -> { + withContext(Dispatchers.IO) { + Gobackend.cleanupConnections() + } + result.success(null) + } else -> result.notImplemented() } } catch (e: Exception) { diff --git a/go_backend/exports.go b/go_backend/exports.go index caf6986b..994397fe 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -239,6 +239,12 @@ func GetDownloadProgress() string { return string(jsonBytes) } +// CleanupConnections closes idle HTTP connections +// Call this periodically during large batch downloads to prevent TCP exhaustion +func CleanupConnections() { + CloseIdleConnections() +} + // SetDownloadDirectory sets the default download directory func SetDownloadDirectory(path string) error { return setDownloadDir(path) diff --git a/go_backend/httputil.go b/go_backend/httputil.go index 5f6e800d..8cf634de 100644 --- a/go_backend/httputil.go +++ b/go_backend/httputil.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "math/rand" + "net" "net/http" "strconv" "time" @@ -41,13 +42,59 @@ const ( DefaultRetryDelay = 1 * time.Second // Initial retry delay ) +// Shared transport with connection pooling to prevent TCP exhaustion +var sharedTransport = &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + MaxConnsPerHost: 20, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + DisableKeepAlives: false, // Enable keep-alives for connection reuse + ForceAttemptHTTP2: true, +} + +// Shared HTTP client for general requests (reuses connections) +var sharedClient = &http.Client{ + Transport: sharedTransport, + Timeout: DefaultTimeout, +} + +// Shared HTTP client for downloads (longer timeout, reuses connections) +var downloadClient = &http.Client{ + Transport: sharedTransport, + Timeout: DownloadTimeout, +} + // NewHTTPClientWithTimeout creates an HTTP client with specified timeout +// Uses shared transport for connection reuse func NewHTTPClientWithTimeout(timeout time.Duration) *http.Client { return &http.Client{ - Timeout: timeout, + Transport: sharedTransport, + Timeout: timeout, } } +// GetSharedClient returns the shared HTTP client for general requests +func GetSharedClient() *http.Client { + return sharedClient +} + +// GetDownloadClient returns the shared HTTP client for downloads +func GetDownloadClient() *http.Client { + return downloadClient +} + +// CloseIdleConnections closes idle connections in the shared transport +// Call this periodically during large batch downloads to prevent connection buildup +func CloseIdleConnections() { + sharedTransport.CloseIdleConnections() +} + // DoRequestWithUserAgent executes an HTTP request with a random User-Agent header func DoRequestWithUserAgent(client *http.Client, req *http.Request) (*http.Response, error) { req.Header.Set("User-Agent", getRandomUserAgent()) diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 5555bf24..d1448c38 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -169,6 +169,8 @@ class DownloadQueueState { // Download Queue Notifier (Riverpod 3.x) class DownloadQueueNotifier extends Notifier { Timer? _progressTimer; + int _downloadCount = 0; // Counter for connection cleanup + static const _cleanupInterval = 50; // Cleanup every 50 downloads @override DownloadQueueState build() { @@ -552,6 +554,17 @@ class DownloadQueueNotifier extends Notifier { error: errorMsg, ); } + + // Increment download counter and cleanup connections periodically + _downloadCount++; + if (_downloadCount % _cleanupInterval == 0) { + print('[DownloadQueue] Cleaning up idle connections (after $_downloadCount downloads)...'); + try { + await PlatformBridge.cleanupConnections(); + } catch (e) { + print('[DownloadQueue] Connection cleanup failed: $e'); + } + } } catch (e, stackTrace) { _stopProgressPolling(); print('[DownloadQueue] Exception: $e'); @@ -565,6 +578,18 @@ class DownloadQueueNotifier extends Notifier { } _stopProgressPolling(); + + // Final cleanup after queue finishes + if (_downloadCount > 0) { + print('[DownloadQueue] Final connection cleanup...'); + try { + await PlatformBridge.cleanupConnections(); + } catch (e) { + print('[DownloadQueue] Final cleanup failed: $e'); + } + _downloadCount = 0; + } + print('[DownloadQueue] Queue processing finished'); state = state.copyWith(isProcessing: false, currentDownload: null); } diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index bc0a6af2..5fdaa097 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -195,4 +195,10 @@ class PlatformBridge { }); return jsonDecode(result as String) as Map; } + + /// Cleanup idle HTTP connections to prevent TCP exhaustion + /// Call this periodically during large batch downloads + static Future cleanupConnections() async { + await _channel.invokeMethod('cleanupConnections'); + } }