fix(cache): harden cache backends, resilience, recovery, and transports

- RESP -ERR no longer closes healthy conn / aborts batch; global cap replacement-aware
- global health-check pass serialized (latent data race); MemoryMonitor via ResourceManager
- token-exchange retry, open-circuit rejections counted as failures
- TLS-posture matching transport reuse (no silent InsecureSkipVerify handoff)
This commit is contained in:
2026-08-14 16:16:51 +01:00
parent 8640451877
commit 4d23d618f5
49 changed files with 2318 additions and 181 deletions
+46 -3
View File
@@ -118,7 +118,7 @@ func DefaultTransportConfig() TransportConfig {
func (p *TransportPool) GetTransport(config TransportConfig) *http.Transport {
// Check client limit
if atomic.LoadInt32(&p.clientCount) >= p.maxClients {
return p.getExistingTransport()
return p.getExistingTransport(config)
}
key := p.configKey(config)
@@ -180,20 +180,41 @@ func (p *TransportPool) ReleaseTransport(transport *http.Transport) {
}
// getExistingTransport returns any available transport when limit is reached
func (p *TransportPool) getExistingTransport() *http.Transport {
func (p *TransportPool) getExistingTransport(config TransportConfig) *http.Transport {
p.mu.RLock()
defer p.mu.RUnlock()
// Prefer a transport whose TLS settings (verification, min version)
// match the request. Reusing a mismatched transport at cap could hand a
// strict-verifying caller an InsecureSkipVerify transport (silent cert
// verification bypass) or an insecure caller a verifying one (R146).
for _, shared := range p.transports {
if shared != nil && shared.transport != nil {
if shared != nil && shared.transport != nil && p.tlsMatches(config, shared.config) {
atomic.AddInt32(&shared.refCount, 1)
shared.lastUsed = time.Now()
return shared.transport
}
}
// No TLS-matching pooled transport: return nil so the caller falls back
// to a safe basic client (http.DefaultTransport verifies certs) rather
// than receiving a mismatched peer. Bounded by the cap (R146).
return nil
}
// tlsMatches reports whether two transports share the same TLS posture
// (certificate verification and minimum protocol version), normalized for
// createTransport's 0-means-TLS1.2 default.
func (p *TransportPool) tlsMatches(a, b TransportConfig) bool {
amin, bmin := a.MinTLSVersion, b.MinTLSVersion
if amin == 0 {
amin = tls.VersionTLS12
}
if bmin == 0 {
bmin = tls.VersionTLS12
}
return a.InsecureSkipVerify == b.InsecureSkipVerify && amin == bmin
}
// createTransport creates a new HTTP transport with the given config
func (p *TransportPool) createTransport(config TransportConfig) *http.Transport {
// Set secure defaults
@@ -276,6 +297,28 @@ func (p *TransportPool) configKey(config TransportConfig) string {
} else {
sb.WriteByte(0)
}
// Include MinTLSVersion and every timeout so two configs differing only
// in TLS minimum version or timeouts do NOT collide on one key and
// silently share a transport with whoever-arrived-first's settings
// (a possible TLS-version downgrade). Mirror http_client_pool.go's
// all-field key (R146).
sb.WriteByte('|')
if config.MinTLSVersion == 0 {
sb.WriteString("tls12")
} else {
sb.WriteString(strconv.Itoa(int(config.MinTLSVersion)))
}
for _, d := range []time.Duration{
config.DialTimeout,
config.TLSHandshakeTimeout,
config.ResponseHeaderTimeout,
config.ExpectContinueTimeout,
config.IdleConnTimeout,
config.KeepAlive,
} {
sb.WriteByte('|')
sb.WriteString(strconv.FormatInt(int64(d), 10))
}
return sb.String()
}