Skip to content

Commit 8c9172e

Browse files
authored
Reuse transport of registry-scanner (#1215)
Signed-off-by: rainsun <[email protected]>
1 parent a7ee15a commit 8c9172e

File tree

3 files changed

+128
-5
lines changed

3 files changed

+128
-5
lines changed

registry-scanner/pkg/registry/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ func clearRegistries() {
3535
registryLock.Lock()
3636
registries = make(map[string]*RegistryEndpoint)
3737
registryLock.Unlock()
38+
39+
// Also clear transport cache when registries are cleared
40+
// This ensures that when registry configuration changes, we use fresh transports
41+
ClearTransportCache()
3842
}
3943

4044
// LoadRegistryConfiguration loads a YAML-formatted registry configuration from

registry-scanner/pkg/registry/endpoints.go

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/image"
1414
"github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log"
1515

16+
memcache "github.com/patrickmn/go-cache"
1617
"go.uber.org/ratelimit"
1718
"golang.org/x/sync/singleflight"
1819
)
@@ -122,6 +123,10 @@ var registryLock sync.RWMutex
122123
// credentialGroup ensures only one credential refresh happens per registry
123124
var credentialGroup singleflight.Group
124125

126+
// Transport cache to avoid creating new transports for each request
127+
// Using go-cache with 30 minute expiration and 10 minute cleanup interval
128+
var transportCache = memcache.New(30*time.Minute, 10*time.Minute)
129+
125130
func AddRegistryEndpointFromConfig(epc RegistryConfiguration) error {
126131
ep := NewRegistryEndpoint(epc.Prefix, epc.Name, epc.ApiURL, epc.Credentials, epc.DefaultNS, epc.Insecure, TagListSortFromString(epc.TagSortMode), epc.Limit, epc.CredsExpire)
127132
return AddRegistryEndpoint(ep)
@@ -308,16 +313,76 @@ func (ep *RegistryEndpoint) DeepCopy() *RegistryEndpoint {
308313
return newEp
309314
}
310315

316+
// ClearTransportCache clears the transport cache
317+
// This is useful when registry configuration changes
318+
func ClearTransportCache() {
319+
transportCache.Flush()
320+
}
321+
311322
// GetTransport returns a transport object for this endpoint
323+
// Implements connection pooling and reuse to avoid creating new transports for each request
312324
func (ep *RegistryEndpoint) GetTransport() *http.Transport {
325+
// Check if we have a cached transport for this registry
326+
if cachedTransport, found := transportCache.Get(ep.RegistryAPI); found {
327+
transport := cachedTransport.(*http.Transport)
328+
log.Debugf("Transport cache HIT for %s: %p", ep.RegistryAPI, transport)
329+
330+
// Validate that the transport is still usable
331+
if isTransportValid(transport) {
332+
return transport
333+
}
334+
335+
// Transport is stale, remove it from cache
336+
log.Debugf("Transport for %s is stale, removing from cache", ep.RegistryAPI)
337+
transportCache.Delete(ep.RegistryAPI)
338+
}
339+
340+
log.Debugf("Transport cache MISS for %s", ep.RegistryAPI)
341+
342+
// Create a new transport with optimized connection pool settings
313343
tlsC := &tls.Config{}
314344
if ep.Insecure {
315345
tlsC.InsecureSkipVerify = true
316346
}
317-
return &http.Transport{
318-
Proxy: http.ProxyFromEnvironment,
319-
TLSClientConfig: tlsC,
347+
348+
// Create transport with aggressive timeout and connection management
349+
transport := &http.Transport{
350+
Proxy: http.ProxyFromEnvironment,
351+
TLSClientConfig: tlsC,
352+
MaxIdleConns: 20, // Reduced global max idle connections
353+
MaxIdleConnsPerHost: 5, // Reduced per-host connections
354+
IdleConnTimeout: 90 * time.Second, // Reduced idle timeout
355+
TLSHandshakeTimeout: 10 * time.Second, // Reduced TLS timeout
356+
ExpectContinueTimeout: 1 * time.Second, // Expect-Continue timeout
357+
DisableKeepAlives: false, // Enable HTTP Keep-Alive
358+
ForceAttemptHTTP2: true, // Enable HTTP/2 if available
359+
// Critical timeout settings to prevent hanging connections
360+
ResponseHeaderTimeout: 10 * time.Second, // Response header timeout
361+
MaxConnsPerHost: 10, // Limit total connections per host
320362
}
363+
364+
// Cache the transport for reuse with default expiration (30 minutes)
365+
transportCache.Set(ep.RegistryAPI, transport, memcache.DefaultExpiration)
366+
log.Debugf("Cached NEW transport for %s: %p", ep.RegistryAPI, transport)
367+
368+
return transport
369+
}
370+
371+
// isTransportValid checks if a cached transport is still valid and usable
372+
func isTransportValid(transport *http.Transport) bool {
373+
// Basic validation - check if transport is not nil and has valid configuration
374+
if transport == nil {
375+
return false
376+
}
377+
378+
// Check if the transport's connection settings are reasonable
379+
// This is a simple validation, more sophisticated checks could be added
380+
if transport.MaxIdleConns < 0 || transport.MaxIdleConnsPerHost < 0 {
381+
return false
382+
}
383+
384+
// Transport appears to be valid
385+
return true
321386
}
322387

323388
// init initializes the registry configuration

registry-scanner/pkg/registry/endpoints_test.go

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package registry
22

33
import (
44
"fmt"
5+
"net/http"
56
"strings"
67
"sync"
78
"testing"
@@ -308,9 +309,12 @@ func Test_GetTagListSortFromString(t *testing.T) {
308309
}
309310

310311
func TestGetTransport(t *testing.T) {
312+
ClearTransportCache()
313+
defer ClearTransportCache()
311314
t.Run("returns transport with default TLS config when Insecure is false", func(t *testing.T) {
312315
endpoint := &RegistryEndpoint{
313-
Insecure: false,
316+
RegistryAPI: "secure-registry",
317+
Insecure: false,
314318
}
315319
transport := endpoint.GetTransport()
316320

@@ -321,7 +325,8 @@ func TestGetTransport(t *testing.T) {
321325

322326
t.Run("returns transport with insecure TLS config when Insecure is true", func(t *testing.T) {
323327
endpoint := &RegistryEndpoint{
324-
Insecure: true,
328+
RegistryAPI: "insecure-registry",
329+
Insecure: true,
325330
}
326331
transport := endpoint.GetTransport()
327332

@@ -373,3 +378,52 @@ func TestAddRegistryEndpointFromConfig(t *testing.T) {
373378
require.NoError(t, err)
374379
})
375380
}
381+
382+
// Test for transport caching and retrieval
383+
func TestTransportCache(t *testing.T) {
384+
// Clean up cache before and after test
385+
ClearTransportCache()
386+
defer ClearTransportCache()
387+
388+
endpoint := &RegistryEndpoint{
389+
RegistryAPI: "https://example.com",
390+
Insecure: false,
391+
}
392+
393+
// 1. Test cache MISS and creation of a new transport
394+
transport1 := endpoint.GetTransport()
395+
assert.NotNil(t, transport1, "Transport should not be nil on cache miss")
396+
397+
// 2. Test cache HIT
398+
transport2 := endpoint.GetTransport()
399+
assert.NotNil(t, transport2, "Transport should not be nil on cache hit")
400+
assert.Same(t, transport1, transport2, "Should retrieve the same transport instance from cache")
401+
402+
// 3. Test cache clearing
403+
ClearTransportCache()
404+
transport3 := endpoint.GetTransport()
405+
assert.NotSame(t, transport1, transport3, "Should create a new transport after cache is cleared")
406+
}
407+
408+
// Test for transport validation logic
409+
func TestIsTransportValid(t *testing.T) {
410+
t.Run("valid transport", func(t *testing.T) {
411+
transport := &http.Transport{
412+
MaxIdleConns: 10,
413+
MaxIdleConnsPerHost: 5,
414+
}
415+
assert.True(t, isTransportValid(transport), "Should be a valid transport")
416+
})
417+
418+
t.Run("nil transport", func(t *testing.T) {
419+
assert.False(t, isTransportValid(nil), "Nil transport should be invalid")
420+
})
421+
422+
t.Run("invalid connection settings", func(t *testing.T) {
423+
transport := &http.Transport{
424+
MaxIdleConns: -1,
425+
}
426+
assert.False(t, isTransportValid(transport), "Transport with invalid settings should be invalid")
427+
})
428+
}
429+

0 commit comments

Comments
 (0)