feat: configure gateway transport pools
This commit is contained in:
parent
d4539da9c5
commit
bda9cc03df
@ -16,6 +16,11 @@ gateway:
|
|||||||
limits:
|
limits:
|
||||||
maxConcurrentConnections: 20000
|
maxConcurrentConnections: 20000
|
||||||
requestsPerMinutePerClient: 60000
|
requestsPerMinutePerClient: 60000
|
||||||
|
transport:
|
||||||
|
maxIdleConns: 20000
|
||||||
|
maxIdleConnsPerHost: 32
|
||||||
|
maxConnsPerHost: 32
|
||||||
|
tunnelBufferBytes: 32768
|
||||||
retry:
|
retry:
|
||||||
maxAttempts: 2
|
maxAttempts: 2
|
||||||
retryMethods: [GET, HEAD]
|
retryMethods: [GET, HEAD]
|
||||||
|
|||||||
@ -333,6 +333,16 @@ gateway:
|
|||||||
auth: {mode: none}
|
auth: {mode: none}
|
||||||
limits:
|
limits:
|
||||||
maxConcurrentConnections: 50000
|
maxConcurrentConnections: 50000
|
||||||
|
transport:
|
||||||
|
dialTimeout: 10s
|
||||||
|
handshakeTimeout: 15s
|
||||||
|
responseHeaderTimeout: 30s
|
||||||
|
idleConnTimeout: 90s
|
||||||
|
maxIdleConns: 20000
|
||||||
|
maxIdleConnsPerHost: 32
|
||||||
|
maxConnsPerHost: 32
|
||||||
|
tunnelBufferBytes: 32768
|
||||||
|
tunnelIdleTimeout: 5m
|
||||||
retry:
|
retry:
|
||||||
maxAttempts: 2
|
maxAttempts: 2
|
||||||
retryMethods: [GET, HEAD]
|
retryMethods: [GET, HEAD]
|
||||||
@ -358,6 +368,31 @@ gateway:
|
|||||||
- 保留地址、CGNAT 与云元数据端点始终拒绝,不能通过私网/链路本地开关放行。
|
- 保留地址、CGNAT 与云元数据端点始终拒绝,不能通过私网/链路本地开关放行。
|
||||||
- `maxConcurrentConnections` 是入口准入上限,不是 Proxy 容量上限。
|
- `maxConcurrentConnections` 是入口准入上限,不是 Proxy 容量上限。
|
||||||
|
|
||||||
|
### 5.1 Gateway 本地传输层
|
||||||
|
|
||||||
|
**gateway.transport** 只配置每个 Gateway Worker 的本地 HTTP Transport 和 CONNECT
|
||||||
|
隧道 I/O,不读取或写入 Redis、PostgreSQL、Provider,也不参与热路径分配。修改
|
||||||
|
这些字段需要滚动重启对应 Gateway Worker 后生效。
|
||||||
|
|
||||||
|
- **dialTimeout**、**handshakeTimeout**、**responseHeaderTimeout**、
|
||||||
|
**idleConnTimeout**:分别限制拨号、上游 TLS/CONNECT 握手、HTTP 响应头和空闲
|
||||||
|
HTTP 连接的时长。
|
||||||
|
- **maxIdleConns**:单个本地 HTTP Transport 的总空闲连接上限。Gateway 分别为代理
|
||||||
|
转发和 direct fallback 建立 Transport,最坏情况下两者都可能保有空闲连接。
|
||||||
|
- **maxIdleConnsPerHost**:单个上游 Host 的空闲 HTTP 连接上限。
|
||||||
|
- **maxConnsPerHost**:单个上游 Host 的 HTTP 活跃加空闲连接上限。它不替代
|
||||||
|
**limits.maxConcurrentConnections**,也不限制已经建立的 CONNECT 隧道。
|
||||||
|
- **tunnelBufferBytes**:每个活跃 CONNECT 隧道的每个复制方向使用一个缓冲区,内存
|
||||||
|
预算至少按 活跃隧道数 * 2 * tunnelBufferBytes 计算。
|
||||||
|
- **tunnelIdleTimeout**:CONNECT 隧道双向没有数据活动时的最长存活时间。
|
||||||
|
|
||||||
|
所有字段均为可选项;数值 0 表示沿用进程默认值:拨号 10s、握手 15s、
|
||||||
|
响应头 30s、空闲 HTTP 连接 90s、总空闲连接 1024、每 Host 空闲连接
|
||||||
|
64、隧道缓冲 32768 字节、隧道空闲 5m。计数和时长不得为负数;
|
||||||
|
maxIdleConnsPerHost 不得大于显式配置的 maxIdleConns。示例中的 20000/32
|
||||||
|
是高并发 HTTP 转发的起点,不是每秒请求量的换算公式,应同时按可用文件描述符、
|
||||||
|
上游限额和观测到的复用率调节。
|
||||||
|
|
||||||
## 6. Distribution
|
## 6. Distribution
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@ -66,6 +66,7 @@ type Listener struct {
|
|||||||
Limits Limits `yaml:"limits"`
|
Limits Limits `yaml:"limits"`
|
||||||
Retry Retry `yaml:"retry"`
|
Retry Retry `yaml:"retry"`
|
||||||
StickySession StickySession `yaml:"stickySession"`
|
StickySession StickySession `yaml:"stickySession"`
|
||||||
|
Transport GatewayTransport `yaml:"transport"`
|
||||||
DestinationPolicy DestinationPolicy `yaml:"destinationPolicy"`
|
DestinationPolicy DestinationPolicy `yaml:"destinationPolicy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,6 +128,20 @@ type StickySession struct {
|
|||||||
MaxEntries int `yaml:"maxEntries"`
|
MaxEntries int `yaml:"maxEntries"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GatewayTransport configures the local Gateway process connection pools and
|
||||||
|
// tunnel I/O. It intentionally contains no remote state or provider settings.
|
||||||
|
type GatewayTransport struct {
|
||||||
|
DialTimeout Duration `yaml:"dialTimeout"`
|
||||||
|
HandshakeTimeout Duration `yaml:"handshakeTimeout"`
|
||||||
|
ResponseHeaderTimeout Duration `yaml:"responseHeaderTimeout"`
|
||||||
|
IdleConnTimeout Duration `yaml:"idleConnTimeout"`
|
||||||
|
MaxIdleConns int `yaml:"maxIdleConns"`
|
||||||
|
MaxIdleConnsPerHost int `yaml:"maxIdleConnsPerHost"`
|
||||||
|
MaxConnsPerHost int `yaml:"maxConnsPerHost"`
|
||||||
|
TunnelBufferBytes int `yaml:"tunnelBufferBytes"`
|
||||||
|
TunnelIdleTimeout Duration `yaml:"tunnelIdleTimeout"`
|
||||||
|
}
|
||||||
|
|
||||||
type DestinationPolicy struct {
|
type DestinationPolicy struct {
|
||||||
DenyPrivateNetworks *bool `yaml:"denyPrivateNetworks"`
|
DenyPrivateNetworks *bool `yaml:"denyPrivateNetworks"`
|
||||||
DenyLoopback *bool `yaml:"denyLoopback"`
|
DenyLoopback *bool `yaml:"denyLoopback"`
|
||||||
|
|||||||
@ -469,6 +469,51 @@ func TestValidateGatewayStickySession(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateGatewayTransport(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cfg := mustLoadValidConfig(t)
|
||||||
|
cfg.Gateway.Transport = GatewayTransport{
|
||||||
|
DialTimeout: Duration(time.Second),
|
||||||
|
HandshakeTimeout: Duration(2 * time.Second),
|
||||||
|
ResponseHeaderTimeout: Duration(3 * time.Second),
|
||||||
|
IdleConnTimeout: Duration(time.Minute),
|
||||||
|
MaxIdleConns: 500,
|
||||||
|
MaxIdleConnsPerHost: 20,
|
||||||
|
MaxConnsPerHost: 10,
|
||||||
|
TunnelBufferBytes: 32 << 10,
|
||||||
|
TunnelIdleTimeout: Duration(5 * time.Minute),
|
||||||
|
}
|
||||||
|
if err := Validate(cfg); err != nil {
|
||||||
|
t.Fatalf("Validate(valid gateway transport) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Gateway.Transport.DialTimeout = Duration(-time.Second)
|
||||||
|
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "transport.dialTimeout") {
|
||||||
|
t.Fatalf("Validate(negative dial timeout) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Gateway.Transport.DialTimeout = Duration(time.Second)
|
||||||
|
cfg.Gateway.Transport.MaxConnsPerHost = -1
|
||||||
|
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "transport.maxConnsPerHost") {
|
||||||
|
t.Fatalf("Validate(negative max connections) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Gateway.Transport.MaxConnsPerHost = 10
|
||||||
|
cfg.Gateway.Transport.MaxIdleConns = 19
|
||||||
|
cfg.Gateway.Transport.MaxIdleConnsPerHost = 20
|
||||||
|
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "maxIdleConnsPerHost") {
|
||||||
|
t.Fatalf("Validate(inconsistent idle pool) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Gateway.Transport.MaxIdleConns = 500
|
||||||
|
cfg.Gateway.Transport.MaxIdleConnsPerHost = 20
|
||||||
|
cfg.Distribution.Transport = GatewayTransport{MaxConnsPerHost: 10}
|
||||||
|
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "distribution transport") {
|
||||||
|
t.Fatalf("Validate(distribution transport) error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateMetricsListener(t *testing.T) {
|
func TestValidateMetricsListener(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
cfg := mustLoadValidConfig(t)
|
cfg := mustLoadValidConfig(t)
|
||||||
|
|||||||
@ -113,6 +113,9 @@ func Validate(cfg *Config) error {
|
|||||||
if err := validateGatewayStickySession(cfg.Gateway); err != nil {
|
if err := validateGatewayStickySession(cfg.Gateway); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := validateGatewayTransport(cfg.Gateway); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if cfg.Distribution.Enabled {
|
if cfg.Distribution.Enabled {
|
||||||
if err := validateDistributionClientPolicies(cfg.Distribution.Auth, cfg.Upstreams); err != nil {
|
if err := validateDistributionClientPolicies(cfg.Distribution.Auth, cfg.Upstreams); err != nil {
|
||||||
@ -121,6 +124,9 @@ func Validate(cfg *Config) error {
|
|||||||
if cfg.Distribution.StickySession.Enabled {
|
if cfg.Distribution.StickySession.Enabled {
|
||||||
return fmt.Errorf("validate distribution stickySession: only supported on gateway")
|
return fmt.Errorf("validate distribution stickySession: only supported on gateway")
|
||||||
}
|
}
|
||||||
|
if gatewayTransportConfigured(cfg.Distribution.Transport) {
|
||||||
|
return fmt.Errorf("validate distribution transport: only supported on gateway")
|
||||||
|
}
|
||||||
clientIdentificationMode := cfg.Distribution.ClientIdentification.Mode
|
clientIdentificationMode := cfg.Distribution.ClientIdentification.Mode
|
||||||
if clientIdentificationMode == "" {
|
if clientIdentificationMode == "" {
|
||||||
clientIdentificationMode = "sourceIP"
|
clientIdentificationMode = "sourceIP"
|
||||||
@ -154,6 +160,9 @@ func Validate(cfg *Config) error {
|
|||||||
if cfg.Admin.Enabled && cfg.Admin.StickySession.Enabled {
|
if cfg.Admin.Enabled && cfg.Admin.StickySession.Enabled {
|
||||||
return fmt.Errorf("validate admin stickySession: only supported on gateway")
|
return fmt.Errorf("validate admin stickySession: only supported on gateway")
|
||||||
}
|
}
|
||||||
|
if cfg.Admin.Enabled && gatewayTransportConfigured(cfg.Admin.Transport) {
|
||||||
|
return fmt.Errorf("validate admin transport: only supported on gateway")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,6 +186,53 @@ func validateGatewayStickySession(listener Listener) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateGatewayTransport(listener Listener) error {
|
||||||
|
item := listener.Transport
|
||||||
|
for _, timeout := range []struct {
|
||||||
|
name string
|
||||||
|
value Duration
|
||||||
|
}{
|
||||||
|
{name: "dialTimeout", value: item.DialTimeout},
|
||||||
|
{name: "handshakeTimeout", value: item.HandshakeTimeout},
|
||||||
|
{name: "responseHeaderTimeout", value: item.ResponseHeaderTimeout},
|
||||||
|
{name: "idleConnTimeout", value: item.IdleConnTimeout},
|
||||||
|
{name: "tunnelIdleTimeout", value: item.TunnelIdleTimeout},
|
||||||
|
} {
|
||||||
|
if timeout.value.Value() < 0 {
|
||||||
|
return fmt.Errorf("validate gateway transport.%s: must be non-negative", timeout.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, limit := range []struct {
|
||||||
|
name string
|
||||||
|
value int
|
||||||
|
}{
|
||||||
|
{name: "maxIdleConns", value: item.MaxIdleConns},
|
||||||
|
{name: "maxIdleConnsPerHost", value: item.MaxIdleConnsPerHost},
|
||||||
|
{name: "maxConnsPerHost", value: item.MaxConnsPerHost},
|
||||||
|
{name: "tunnelBufferBytes", value: item.TunnelBufferBytes},
|
||||||
|
} {
|
||||||
|
if limit.value < 0 || limit.value > MaximumPoolSize {
|
||||||
|
return fmt.Errorf("validate gateway transport.%s: must be in [0, %d]", limit.name, MaximumPoolSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if item.MaxIdleConns > 0 && item.MaxIdleConnsPerHost > item.MaxIdleConns {
|
||||||
|
return fmt.Errorf("validate gateway transport.maxIdleConnsPerHost: must not exceed maxIdleConns")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gatewayTransportConfigured(item GatewayTransport) bool {
|
||||||
|
return item.DialTimeout != 0 ||
|
||||||
|
item.HandshakeTimeout != 0 ||
|
||||||
|
item.ResponseHeaderTimeout != 0 ||
|
||||||
|
item.IdleConnTimeout != 0 ||
|
||||||
|
item.MaxIdleConns != 0 ||
|
||||||
|
item.MaxIdleConnsPerHost != 0 ||
|
||||||
|
item.MaxConnsPerHost != 0 ||
|
||||||
|
item.TunnelBufferBytes != 0 ||
|
||||||
|
item.TunnelIdleTimeout != 0
|
||||||
|
}
|
||||||
|
|
||||||
func validCanonicalHeaderName(value string) bool {
|
func validCanonicalHeaderName(value string) bool {
|
||||||
if value == "" || http.CanonicalHeaderKey(value) != value {
|
if value == "" || http.CanonicalHeaderKey(value) != value {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -156,7 +156,7 @@ func newRuntime(ctx context.Context, configuration *config.Config, options Optio
|
|||||||
storeOptions.CapacityInvariantObserver = collector
|
storeOptions.CapacityInvariantObserver = collector
|
||||||
}
|
}
|
||||||
store := snapshot.NewStoreWithOptions(options.ClusterID, options.WorkerID, storeOptions)
|
store := snapshot.NewStoreWithOptions(options.ClusterID, options.WorkerID, storeOptions)
|
||||||
proxyTransport := transport.New(transport.Config{}, snapshotCredentialResolver{store: store})
|
proxyTransport := transport.New(gatewayTransportConfig(configuration.Gateway), snapshotCredentialResolver{store: store})
|
||||||
outcomes, err := gatewayOutcome.NewQueue(gatewayOutcome.QueueOptions{
|
outcomes, err := gatewayOutcome.NewQueue(gatewayOutcome.QueueOptions{
|
||||||
Capacity: defaultOutcomeQueueCapacity, MaxBatch: min(defaultOutcomeBatchSize, configuration.ControlPlane.MaxRuntimeCounters),
|
Capacity: defaultOutcomeQueueCapacity, MaxBatch: min(defaultOutcomeBatchSize, configuration.ControlPlane.MaxRuntimeCounters),
|
||||||
Metrics: outcomeMetrics,
|
Metrics: outcomeMetrics,
|
||||||
@ -243,6 +243,21 @@ func newRuntime(ctx context.Context, configuration *config.Config, options Optio
|
|||||||
return &runtime{connection: connection, group: group}, nil
|
return &runtime{connection: connection, group: group}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func gatewayTransportConfig(listener config.Listener) transport.Config {
|
||||||
|
item := listener.Transport
|
||||||
|
return transport.Config{
|
||||||
|
DialTimeout: item.DialTimeout.Value(),
|
||||||
|
HandshakeTimeout: item.HandshakeTimeout.Value(),
|
||||||
|
ResponseHeaderTimeout: item.ResponseHeaderTimeout.Value(),
|
||||||
|
IdleConnTimeout: item.IdleConnTimeout.Value(),
|
||||||
|
MaxIdleConns: item.MaxIdleConns,
|
||||||
|
MaxIdleConnsPerHost: item.MaxIdleConnsPerHost,
|
||||||
|
MaxConnsPerHost: item.MaxConnsPerHost,
|
||||||
|
TunnelBufferBytes: item.TunnelBufferBytes,
|
||||||
|
TunnelIdleTimeout: item.TunnelIdleTimeout.Value(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type generatedRuntimeClient struct {
|
type generatedRuntimeClient struct {
|
||||||
client controlplanev1.WorkerControlPlaneClient
|
client controlplanev1.WorkerControlPlaneClient
|
||||||
}
|
}
|
||||||
|
|||||||
@ -145,6 +145,34 @@ func TestControlPlaneTransportRequiresDedicatedGatewayTLS(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGatewayTransportConfigMapsListenerSettings(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
got := gatewayTransportConfig(config.Listener{Transport: config.GatewayTransport{
|
||||||
|
DialTimeout: config.Duration(time.Second),
|
||||||
|
HandshakeTimeout: config.Duration(2 * time.Second),
|
||||||
|
ResponseHeaderTimeout: config.Duration(3 * time.Second),
|
||||||
|
IdleConnTimeout: config.Duration(time.Minute),
|
||||||
|
MaxIdleConns: 500,
|
||||||
|
MaxIdleConnsPerHost: 20,
|
||||||
|
MaxConnsPerHost: 10,
|
||||||
|
TunnelBufferBytes: 32 << 10,
|
||||||
|
TunnelIdleTimeout: config.Duration(5 * time.Minute),
|
||||||
|
}})
|
||||||
|
|
||||||
|
if got.DialTimeout != time.Second ||
|
||||||
|
got.HandshakeTimeout != 2*time.Second ||
|
||||||
|
got.ResponseHeaderTimeout != 3*time.Second ||
|
||||||
|
got.IdleConnTimeout != time.Minute ||
|
||||||
|
got.MaxIdleConns != 500 ||
|
||||||
|
got.MaxIdleConnsPerHost != 20 ||
|
||||||
|
got.MaxConnsPerHost != 10 ||
|
||||||
|
got.TunnelBufferBytes != 32<<10 ||
|
||||||
|
got.TunnelIdleTimeout != 5*time.Minute {
|
||||||
|
t.Fatalf("gatewayTransportConfig() = %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSnapshotReadinessRequiresCurrentSnapshot(t *testing.T) {
|
func TestSnapshotReadinessRequiresCurrentSnapshot(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,7 @@ type Config struct {
|
|||||||
IdleConnTimeout time.Duration
|
IdleConnTimeout time.Duration
|
||||||
MaxIdleConns int
|
MaxIdleConns int
|
||||||
MaxIdleConnsPerHost int
|
MaxIdleConnsPerHost int
|
||||||
|
MaxConnsPerHost int
|
||||||
MaxErrorResponseBytes int64
|
MaxErrorResponseBytes int64
|
||||||
MaxResponseHeaderBytes int64
|
MaxResponseHeaderBytes int64
|
||||||
TunnelBufferBytes int
|
TunnelBufferBytes int
|
||||||
@ -120,6 +121,7 @@ func newHTTPTransport(config Config, proxy func(*http.Request) (*url.URL, error)
|
|||||||
ForceAttemptHTTP2: true,
|
ForceAttemptHTTP2: true,
|
||||||
MaxIdleConns: config.MaxIdleConns,
|
MaxIdleConns: config.MaxIdleConns,
|
||||||
MaxIdleConnsPerHost: config.MaxIdleConnsPerHost,
|
MaxIdleConnsPerHost: config.MaxIdleConnsPerHost,
|
||||||
|
MaxConnsPerHost: config.MaxConnsPerHost,
|
||||||
IdleConnTimeout: config.IdleConnTimeout,
|
IdleConnTimeout: config.IdleConnTimeout,
|
||||||
TLSHandshakeTimeout: config.HandshakeTimeout,
|
TLSHandshakeTimeout: config.HandshakeTimeout,
|
||||||
ResponseHeaderTimeout: config.ResponseHeaderTimeout,
|
ResponseHeaderTimeout: config.ResponseHeaderTimeout,
|
||||||
|
|||||||
@ -88,6 +88,32 @@ func TestRoundTripDirectForwardsWithoutProxyAuthorization(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewAppliesConfiguredConnectionPoolLimits(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client := New(Config{
|
||||||
|
MaxIdleConns: 256,
|
||||||
|
MaxIdleConnsPerHost: 24,
|
||||||
|
MaxConnsPerHost: 12,
|
||||||
|
}, nil)
|
||||||
|
t.Cleanup(client.CloseIdleConnections)
|
||||||
|
|
||||||
|
for name, item := range map[string]*http.Transport{
|
||||||
|
"proxy": client.client,
|
||||||
|
"direct": client.direct,
|
||||||
|
} {
|
||||||
|
if item.MaxIdleConns != 256 {
|
||||||
|
t.Fatalf("%s MaxIdleConns = %d, want 256", name, item.MaxIdleConns)
|
||||||
|
}
|
||||||
|
if item.MaxIdleConnsPerHost != 24 {
|
||||||
|
t.Fatalf("%s MaxIdleConnsPerHost = %d, want 24", name, item.MaxIdleConnsPerHost)
|
||||||
|
}
|
||||||
|
if item.MaxConnsPerHost != 12 {
|
||||||
|
t.Fatalf("%s MaxConnsPerHost = %d, want 12", name, item.MaxConnsPerHost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOpenDirectTunnelDialsTarget(t *testing.T) {
|
func TestOpenDirectTunnelDialsTarget(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user