From c01b4b663050e13476f2e3f205d0e92467c67528 Mon Sep 17 00:00:00 2001 From: Maxim Date: Wed, 5 Aug 2026 20:04:29 +0300 Subject: [PATCH] Fix service monitor connection leak --- services.go | 18 +++++++++++++++--- services_test.go | 17 ++++++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/services.go b/services.go index 0457a9c..49ab9d4 100644 --- a/services.go +++ b/services.go @@ -74,6 +74,20 @@ type ServiceHistoryPoint struct { LatencyMS float64 `json:"latencyMs"` } +var serviceHTTPTransport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 32, + MaxIdleConnsPerHost: 2, + IdleConnTimeout: 30 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: time.Second, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12}, +} + +var serviceHTTPClient = &http.Client{Transport: serviceHTTPTransport} + func (s *Store) Services() ([]MonitoredService, error) { rows, err := s.db.Query(`SELECT id,name,url,local_address,check_type,expected_code,expected_text,timeout_seconds,enabled FROM services ORDER BY id`) if err != nil { @@ -296,14 +310,12 @@ func checkServiceEndpoint(v MonitoredService, kind, label, target string) Servic st.Error = strings.TrimSpace(string(out)) } default: - tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12}} - client := &http.Client{Transport: tr, Timeout: time.Duration(v.TimeoutSeconds) * time.Second} req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil) if err != nil { st.Error = err.Error() break } - resp, err := client.Do(req) + resp, err := serviceHTTPClient.Do(req) if err != nil { st.Error = err.Error() } else { diff --git a/services_test.go b/services_test.go index 32b5f42..c939f73 100644 --- a/services_test.go +++ b/services_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "testing" + "time" +) func TestServiceCRUD(t *testing.T) { store, err := openStore(":memory:") @@ -38,3 +41,15 @@ func TestValidateServiceChecksBothHTTPAddresses(t *testing.T) { t.Fatal(err) } } + +func TestHTTPServiceTransportHasBoundedIdlePool(t *testing.T) { + if serviceHTTPClient.Transport != serviceHTTPTransport { + t.Fatal("проверки сервисов должны использовать общий HTTP transport") + } + if serviceHTTPTransport.IdleConnTimeout < time.Second { + t.Fatal("idle-соединения должны автоматически закрываться") + } + if serviceHTTPTransport.MaxIdleConnsPerHost < 1 || serviceHTTPTransport.MaxIdleConnsPerHost > 4 { + t.Fatalf("небезопасный размер пула на хост: %d", serviceHTTPTransport.MaxIdleConnsPerHost) + } +}