mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-09 17:42:08 +08:00
- Configure upstream connection pool under antigravity.connection-pool with enabled: false by default. - In short connection mode, set MaxIdleConnsPerHost = -1 with DisableKeepAlives = false, ensuring immediate TCP termination after response body completion without leaking Connection: close request headers. - When pooling is explicitly enabled (enabled: true), cap idle-conn-timeout at 210s (leaving a 30s safety buffer below Google Frontend's 240s Keep-Alive cutoff) and default max-idle-conns-per-host to 2 (bounded at 100). - Refactor TransportCache to execute CloseIdleConnections outside the mutex lock during LRU eviction and matching closes. - Proactively evict and close idle connections on 429 quota exhaustion across Execute, ExecuteStream, and CountTokens. - Wire hot-reload diff detection and server reload purge hooks for graceful transport pool updates.
208 lines
5.9 KiB
Go
208 lines
5.9 KiB
Go
package helps
|
|
|
|
import (
|
|
"container/list"
|
|
"errors"
|
|
"net/http"
|
|
"sync"
|
|
)
|
|
|
|
// DefaultTransportCacheCapacity bounds how many transports a TransportCache keeps
|
|
// alive at once. Every cached transport owns an independent connection pool, so an
|
|
// unbounded cache would let idle sockets and the goroutines managing them grow
|
|
// without limit whenever keys churn, for example when a credential's proxy is
|
|
// rotated through the management API or when an SDK embedder supplies a freshly
|
|
// built base transport per request.
|
|
const DefaultTransportCacheCapacity = 64
|
|
|
|
// TransportCache memoizes HTTP transports under a comparable key using a bounded
|
|
// LRU. Evicting an entry closes its idle connections so neither the pool nor its
|
|
// background goroutines outlive the cache entry.
|
|
//
|
|
// The key type is generic so callers can mix value identity (a normalized proxy
|
|
// URL) with pointer identity (a base transport supplied by the caller) without the
|
|
// cache retaining either beyond the LRU window.
|
|
type TransportCache[K comparable] struct {
|
|
mu sync.Mutex
|
|
capacity int
|
|
// order keeps the most recently used entry at the front.
|
|
order *list.List
|
|
items map[K]*list.Element
|
|
}
|
|
|
|
type transportCacheEntry[K comparable] struct {
|
|
key K
|
|
transport *http.Transport
|
|
}
|
|
|
|
// NewTransportCache returns a cache holding at most capacity transports. A
|
|
// non-positive capacity falls back to DefaultTransportCacheCapacity.
|
|
func NewTransportCache[K comparable](capacity int) *TransportCache[K] {
|
|
if capacity <= 0 {
|
|
capacity = DefaultTransportCacheCapacity
|
|
}
|
|
return &TransportCache[K]{
|
|
capacity: capacity,
|
|
order: list.New(),
|
|
items: make(map[K]*list.Element, capacity),
|
|
}
|
|
}
|
|
|
|
// Get returns the transport cached under key, calling build on the first use of
|
|
// that key. Concurrent callers observe the same instance.
|
|
//
|
|
// A build error is propagated without being cached, so a later call can retry and
|
|
// a failed lookup never occupies a cache slot. build must not call back into the
|
|
// same cache.
|
|
func (c *TransportCache[K]) Get(key K, build func() (*http.Transport, error)) (*http.Transport, error) {
|
|
if c == nil {
|
|
return nil, errors.New("transport cache: nil cache")
|
|
}
|
|
if build == nil {
|
|
return nil, errors.New("transport cache: nil build function")
|
|
}
|
|
|
|
c.mu.Lock()
|
|
if element, ok := c.items[key]; ok {
|
|
c.order.MoveToFront(element)
|
|
transport := element.Value.(*transportCacheEntry[K]).transport
|
|
c.mu.Unlock()
|
|
return transport, nil
|
|
}
|
|
|
|
transport, errBuild := build()
|
|
if errBuild != nil {
|
|
c.mu.Unlock()
|
|
return nil, errBuild
|
|
}
|
|
if transport == nil {
|
|
c.mu.Unlock()
|
|
return nil, errors.New("transport cache: build returned no transport")
|
|
}
|
|
|
|
// Double check in case key was populated while build was running
|
|
if element, ok := c.items[key]; ok {
|
|
c.order.MoveToFront(element)
|
|
existing := element.Value.(*transportCacheEntry[K]).transport
|
|
c.mu.Unlock()
|
|
transport.CloseIdleConnections()
|
|
return existing, nil
|
|
}
|
|
|
|
c.items[key] = c.order.PushFront(&transportCacheEntry[K]{key: key, transport: transport})
|
|
evicted := c.evictLocked()
|
|
c.mu.Unlock()
|
|
|
|
for _, t := range evicted {
|
|
t.CloseIdleConnections()
|
|
}
|
|
return transport, nil
|
|
}
|
|
|
|
// evictLocked drops least recently used entries until the cache fits its capacity,
|
|
// returning a slice of evicted transports to be closed outside the lock.
|
|
// Closing idle connections is what actually releases the evicted pool; in-flight
|
|
// requests still holding the transport are unaffected because CloseIdleConnections
|
|
// only reaps connections that are currently idle.
|
|
func (c *TransportCache[K]) evictLocked() []*http.Transport {
|
|
var evicted []*http.Transport
|
|
for c.order.Len() > c.capacity {
|
|
oldest := c.order.Back()
|
|
if oldest == nil {
|
|
break
|
|
}
|
|
c.order.Remove(oldest)
|
|
entry := oldest.Value.(*transportCacheEntry[K])
|
|
delete(c.items, entry.key)
|
|
evicted = append(evicted, entry.transport)
|
|
}
|
|
return evicted
|
|
}
|
|
|
|
// Len reports how many transports the cache currently holds.
|
|
func (c *TransportCache[K]) Len() int {
|
|
if c == nil {
|
|
return 0
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.order.Len()
|
|
}
|
|
|
|
// Contains reports whether key exists in the cache.
|
|
func (c *TransportCache[K]) Contains(key K) bool {
|
|
if c == nil {
|
|
return false
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
_, ok := c.items[key]
|
|
return ok
|
|
}
|
|
|
|
// CloseKey removes the transport cached under key (if present) and closes its idle connections.
|
|
// Returns true if the entry was found and closed.
|
|
func (c *TransportCache[K]) CloseKey(key K) bool {
|
|
if c == nil {
|
|
return false
|
|
}
|
|
c.mu.Lock()
|
|
element, ok := c.items[key]
|
|
if !ok {
|
|
c.mu.Unlock()
|
|
return false
|
|
}
|
|
c.order.Remove(element)
|
|
delete(c.items, key)
|
|
transport := element.Value.(*transportCacheEntry[K]).transport
|
|
c.mu.Unlock()
|
|
|
|
transport.CloseIdleConnections()
|
|
return true
|
|
}
|
|
|
|
// CloseMatching removes and closes idle connections for every cached transport whose key
|
|
// satisfies predicate. Returns the number of closed transports.
|
|
func (c *TransportCache[K]) CloseMatching(predicate func(key K) bool) int {
|
|
if c == nil || predicate == nil {
|
|
return 0
|
|
}
|
|
c.mu.Lock()
|
|
var toClose []*http.Transport
|
|
var next *list.Element
|
|
for element := c.order.Front(); element != nil; element = next {
|
|
next = element.Next()
|
|
entry := element.Value.(*transportCacheEntry[K])
|
|
if predicate(entry.key) {
|
|
c.order.Remove(element)
|
|
delete(c.items, entry.key)
|
|
toClose = append(toClose, entry.transport)
|
|
}
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
for _, t := range toClose {
|
|
t.CloseIdleConnections()
|
|
}
|
|
return len(toClose)
|
|
}
|
|
|
|
// Purge drops every entry and closes the idle connections it was holding.
|
|
func (c *TransportCache[K]) Purge() {
|
|
if c == nil {
|
|
return
|
|
}
|
|
c.mu.Lock()
|
|
var toClose []*http.Transport
|
|
for element := c.order.Front(); element != nil; element = element.Next() {
|
|
toClose = append(toClose, element.Value.(*transportCacheEntry[K]).transport)
|
|
}
|
|
c.order.Init()
|
|
c.items = make(map[K]*list.Element, c.capacity)
|
|
c.mu.Unlock()
|
|
|
|
for _, t := range toClose {
|
|
t.CloseIdleConnections()
|
|
}
|
|
}
|