This is an automated email from the ASF dual-hosted git repository.
AlexStocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git
The following commit(s) were added to refs/heads/develop by this push:
new d4733b12b feat(triple): share HTTP/3 QUIC config with client (#3620)
d4733b12b is described below
commit d4733b12bf965e4be1516de5164833ec007aa7c3
Author: CAICAII <[email protected]>
AuthorDate: Fri Aug 14 12:27:06 2026 +0800
feat(triple): share HTTP/3 QUIC config with client (#3620)
* feat(triple): share http3 quic config with client
Signed-off-by: CAICAIIs <[email protected]>
---
protocol/triple/client.go | 34 ++++--
protocol/triple/client_external_test.go | 30 +++++
protocol/triple/client_test.go | 29 ++++-
protocol/triple/dual_transport.go | 24 +++-
.../http3config}/http3_config.go | 9 +-
.../internal/http3config/http3_config_test.go | 134 +++++++++++++++++++++
protocol/triple/triple_protocol/server.go | 5 +-
protocol/triple/triple_protocol/server_test.go | 46 -------
8 files changed, 243 insertions(+), 68 deletions(-)
diff --git a/protocol/triple/client.go b/protocol/triple/client.go
index 382b3fa79..6aa949609 100644
--- a/protocol/triple/client.go
+++ b/protocol/triple/client.go
@@ -45,6 +45,7 @@ import (
"dubbo.apache.org/dubbo-go/v3/common"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"dubbo.apache.org/dubbo-go/v3/global"
+ "dubbo.apache.org/dubbo-go/v3/protocol/triple/internal/http3config"
tri "dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol"
dubbotls "dubbo.apache.org/dubbo-go/v3/tls"
)
@@ -226,27 +227,40 @@ func newClientManager(url *common.URL) (*clientManager,
error) {
return nil, fmt.Errorf("TRIPLE http3 client must have
TLS config, but TLS config is nil")
}
- // HTTP/3 transport maps keepalive to quic-go's KeepAlivePeriod
and MaxIdleTimeout;
- // All other QUIC knobs keep the quic.Config defaults.
+ var http3Config *global.Http3Config
+ if tripleConf != nil {
+ http3Config = tripleConf.Http3
+ }
+ quicConfig, configErr := http3config.NewQUICConfig(http3Config,
&quic.Config{
+ KeepAlivePeriod: keepAliveInterval,
+ MaxIdleTimeout: keepAliveTimeout,
+ })
+ if configErr != nil {
+ return nil, configErr
+ }
+
transport = &http3.Transport{
TLSClientConfig: cfg,
- QUICConfig: &quic.Config{
- // ref:
https://quic-go.net/docs/quic/connection/#keeping-a-connection-alive
- KeepAlivePeriod: keepAliveInterval,
- // ref:
https://quic-go.net/docs/quic/connection/#idle-timeout
- MaxIdleTimeout: keepAliveTimeout,
- },
+ QUICConfig: quicConfig,
}
-
logger.Info("[Triple][Client] triple http3 client transport
init successfully")
case constant.CallHTTP2AndHTTP3:
if !tlsFlag {
return nil, fmt.Errorf("TRIPLE HTTP/2 and HTTP/3 client
must have TLS config, but TLS config is nil")
}
+ var http3Config *global.Http3Config
+ if tripleConf != nil {
+ http3Config = tripleConf.Http3
+ }
+
// Dual transport lets the client negotiate HTTP/2 or HTTP/3
with the
// same URL and keepalive settings.
- transport = newDualTransport(cfg, keepAliveInterval,
keepAliveTimeout)
+ dualTransport, configErr := newDualTransport(cfg, http3Config,
keepAliveInterval, keepAliveTimeout)
+ if configErr != nil {
+ return nil, configErr
+ }
+ transport = dualTransport
logger.Info("[Triple][Client] triple HTTP/2 and HTTP/3 client
transport init successfully")
default:
return nil, fmt.Errorf("unsupported http protocol: %s",
callProtocol)
diff --git a/protocol/triple/client_external_test.go
b/protocol/triple/client_external_test.go
index bfbd4d641..1a8d01898 100644
--- a/protocol/triple/client_external_test.go
+++ b/protocol/triple/client_external_test.go
@@ -132,6 +132,36 @@ func writePEMFile(filename, blockType string, data []byte)
error {
})
}
+func TestReferRejectsInvalidHTTP3ConfigOnDualTransportPath(t *testing.T) {
+ caCertFile, serverCertFile, serverKeyFile, cleanup :=
generateTestCerts(t)
+ defer cleanup()
+
+ proto := triple.GetProtocol()
+ url, err := common.NewURL(
+ "tri://localhost:20000",
+ common.WithMethods([]string{"test"}),
+ common.WithProtocol(triple.TRIPLE),
+ common.WithParamsValue(constant.IDLMode, constant.NONIDL),
+ common.WithParamsValue(constant.SerializationKey,
constant.MsgpackSerialization),
+ )
+ require.NoError(t, err)
+ url.SetAttribute(constant.TLSConfigKey, &global.TLSConfig{
+ CACertFile: caCertFile,
+ TLSCertFile: serverCertFile,
+ TLSKeyFile: serverKeyFile,
+ })
+ url.SetAttribute(constant.TripleConfigKey, &global.TripleConfig{
+ Http3: &global.Http3Config{
+ Enable: true,
+ KeepAlivePeriod: "invalid",
+ },
+ })
+ url.SetParam(constant.SslEnabledKey, "true")
+
+ invoker := proto.Refer(url)
+ require.Nil(t, invoker)
+}
+
// testClientInvokeWithTimeout tests that a Triple client invocation correctly
times out.
func testClientInvokeWithTimeout(t *testing.T, tlsConfig *global.TLSConfig) {
diff --git a/protocol/triple/client_test.go b/protocol/triple/client_test.go
index bebf97518..01831a79e 100644
--- a/protocol/triple/client_test.go
+++ b/protocol/triple/client_test.go
@@ -26,6 +26,8 @@ import (
)
import (
+ "github.com/quic-go/quic-go/http3"
+
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -86,7 +88,8 @@ func TestDualTransport(t *testing.T) {
keepAliveTimeout := 5 * time.Second
// Test newDualTransport function
- transport := newDualTransport(nil, keepAliveInterval, keepAliveTimeout)
+ transport, err := newDualTransport(nil, nil, keepAliveInterval,
keepAliveTimeout)
+ require.NoError(t, err)
assert.NotNil(t, transport)
// Verify that transport implements http.RoundTripper interface
@@ -479,7 +482,8 @@ func TestDualTransport_Structure(t *testing.T) {
keepAliveInterval := 30 * time.Second
keepAliveTimeout := 5 * time.Second
- transport := newDualTransport(nil, keepAliveInterval, keepAliveTimeout)
+ transport, err := newDualTransport(nil, nil, keepAliveInterval,
keepAliveTimeout)
+ require.NoError(t, err)
assert.NotNil(t, transport)
dt, ok := transport.(*dualTransport)
@@ -489,6 +493,27 @@ func TestDualTransport_Structure(t *testing.T) {
assert.NotNil(t, dt.altSvcCache)
}
+func TestDualTransport_HTTP3ConfigOverridesKeepAliveDefaults(t *testing.T) {
+ transport, err := newDualTransport(nil, &global.Http3Config{
+ KeepAlivePeriod: "45s",
+ MaxIdleTimeout: "6s",
+ MaxIncomingStreams: 11,
+ MaxIncomingUniStreams: 12,
+ }, 30*time.Second, 5*time.Second)
+ require.NoError(t, err)
+
+ dt, ok := transport.(*dualTransport)
+ require.True(t, ok)
+
+ http3Transport, ok := dt.http3Transport.(*http3.Transport)
+ require.True(t, ok)
+ require.NotNil(t, http3Transport.QUICConfig)
+ assert.Equal(t, 45*time.Second,
http3Transport.QUICConfig.KeepAlivePeriod)
+ assert.Equal(t, 6*time.Second, http3Transport.QUICConfig.MaxIdleTimeout)
+ assert.Equal(t, int64(11), http3Transport.QUICConfig.MaxIncomingStreams)
+ assert.Equal(t, int64(12),
http3Transport.QUICConfig.MaxIncomingUniStreams)
+}
+
func Test_newClientManager_HTTP2WithTLS(t *testing.T) {
// This test requires valid TLS config files
// Skip if files don't exist
diff --git a/protocol/triple/dual_transport.go
b/protocol/triple/dual_transport.go
index f963d158f..583242183 100644
--- a/protocol/triple/dual_transport.go
+++ b/protocol/triple/dual_transport.go
@@ -39,6 +39,8 @@ import (
import (
"dubbo.apache.org/dubbo-go/v3/common/constant"
+ "dubbo.apache.org/dubbo-go/v3/global"
+ "dubbo.apache.org/dubbo-go/v3/protocol/triple/internal/http3config"
tri "dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol"
)
@@ -89,19 +91,29 @@ type dualTransport struct {
}
// newDualTransport creates a new dual transport that supports both HTTP/2 and
HTTP/3
-func newDualTransport(tlsConfig *tls.Config, keepAliveInterval,
keepAliveTimeout time.Duration) http.RoundTripper {
+func newDualTransport(
+ tlsConfig *tls.Config,
+ http3Config *global.Http3Config,
+ keepAliveInterval,
+ keepAliveTimeout time.Duration,
+) (http.RoundTripper, error) {
http2Transport := &http2.Transport{
TLSClientConfig: tlsConfig,
ReadIdleTimeout: keepAliveInterval,
PingTimeout: keepAliveTimeout,
}
+ quicConfig, err := http3config.NewQUICConfig(http3Config, &quic.Config{
+ KeepAlivePeriod: keepAliveInterval,
+ MaxIdleTimeout: keepAliveTimeout,
+ })
+ if err != nil {
+ return nil, err
+ }
+
http3Transport := &http3.Transport{
TLSClientConfig: tlsConfig,
- QUICConfig: &quic.Config{
- KeepAlivePeriod: keepAliveInterval,
- MaxIdleTimeout: keepAliveTimeout,
- },
+ QUICConfig: quicConfig,
}
return &dualTransport{
@@ -111,7 +123,7 @@ func newDualTransport(tlsConfig *tls.Config,
keepAliveInterval, keepAliveTimeout
probeTimeout: defaultH3ProbeTimeout,
baseCooldown: defaultH3BaseCooldown,
maxCooldown: defaultH3MaxCooldown,
- }
+ }, nil
}
// RoundTrip implements http.RoundTripper interface with HTTP Alternative
Services support
diff --git a/protocol/triple/triple_protocol/http3_config.go
b/protocol/triple/internal/http3config/http3_config.go
similarity index 86%
rename from protocol/triple/triple_protocol/http3_config.go
rename to protocol/triple/internal/http3config/http3_config.go
index 93a520ad8..89bf86964 100644
--- a/protocol/triple/triple_protocol/http3_config.go
+++ b/protocol/triple/internal/http3config/http3_config.go
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package triple_protocol
+package http3config
import (
"fmt"
@@ -30,8 +30,13 @@ import (
"dubbo.apache.org/dubbo-go/v3/global"
)
-func newQUICConfig(http3Config *global.Http3Config) (*quic.Config, error) {
+// NewQUICConfig maps HTTP/3 transport configuration over optional default
QUIC settings.
+func NewQUICConfig(http3Config *global.Http3Config, defaults *quic.Config)
(*quic.Config, error) {
quicConfig := &quic.Config{}
+ if defaults != nil {
+ quicConfigCopy := *defaults
+ quicConfig = &quicConfigCopy
+ }
if http3Config == nil {
return quicConfig, nil
}
diff --git a/protocol/triple/internal/http3config/http3_config_test.go
b/protocol/triple/internal/http3config/http3_config_test.go
new file mode 100644
index 000000000..2d3982d8c
--- /dev/null
+++ b/protocol/triple/internal/http3config/http3_config_test.go
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package http3config
+
+import (
+ "testing"
+ "time"
+)
+
+import (
+ "github.com/quic-go/quic-go"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+import (
+ "dubbo.apache.org/dubbo-go/v3/global"
+)
+
+func TestNewQUICConfig(t *testing.T) {
+ t.Run("defaults_preserved_when_unset", func(t *testing.T) {
+ quicConfig, err := NewQUICConfig(&global.Http3Config{}, nil)
+ require.NoError(t, err)
+ require.NotNil(t, quicConfig)
+ assert.Zero(t, quicConfig.KeepAlivePeriod)
+ assert.Zero(t, quicConfig.MaxIdleTimeout)
+ assert.Zero(t, quicConfig.MaxIncomingStreams)
+ assert.Zero(t, quicConfig.MaxIncomingUniStreams)
+ })
+
+ t.Run("explicit_fields_are_mapped", func(t *testing.T) {
+ quicConfig, err := NewQUICConfig(&global.Http3Config{
+ KeepAlivePeriod: "15s",
+ MaxIdleTimeout: "30s",
+ MaxIncomingStreams: 128,
+ MaxIncomingUniStreams: 64,
+ }, nil)
+ require.NoError(t, err)
+ require.NotNil(t, quicConfig)
+ assert.Equal(t, 15*time.Second, quicConfig.KeepAlivePeriod)
+ assert.Equal(t, 30*time.Second, quicConfig.MaxIdleTimeout)
+ assert.Equal(t, int64(128), quicConfig.MaxIncomingStreams)
+ assert.Equal(t, int64(64), quicConfig.MaxIncomingUniStreams)
+ })
+
+ t.Run("invalid_keep_alive_period_returns_error", func(t *testing.T) {
+ quicConfig, err := NewQUICConfig(&global.Http3Config{
+ KeepAlivePeriod: "invalid",
+ }, nil)
+ require.Error(t, err)
+ assert.Nil(t, quicConfig)
+ assert.ErrorContains(t, err, "keep-alive-period")
+ })
+
+ t.Run("invalid_max_idle_timeout_returns_error", func(t *testing.T) {
+ quicConfig, err := NewQUICConfig(&global.Http3Config{
+ MaxIdleTimeout: "invalid",
+ }, nil)
+ require.Error(t, err)
+ assert.Nil(t, quicConfig)
+ assert.ErrorContains(t, err, "max-idle-timeout")
+ })
+ t.Run("nil_config_uses_defaults", func(t *testing.T) {
+ defaults := &quic.Config{
+ KeepAlivePeriod: 10 * time.Second,
+ MaxIdleTimeout: 20 * time.Second,
+ }
+
+ quicConfig, err := NewQUICConfig(nil, defaults)
+ require.NoError(t, err)
+ require.NotNil(t, quicConfig)
+ assert.NotSame(t, defaults, quicConfig)
+ assert.Equal(t, 10*time.Second, quicConfig.KeepAlivePeriod)
+ assert.Equal(t, 20*time.Second, quicConfig.MaxIdleTimeout)
+ })
+
+ t.Run("defaults_are_used_when_fields_unset", func(t *testing.T) {
+ defaults := &quic.Config{
+ KeepAlivePeriod: 10 * time.Second,
+ MaxIdleTimeout: 20 * time.Second,
+ }
+
+ quicConfig, err := NewQUICConfig(&global.Http3Config{},
defaults)
+ require.NoError(t, err)
+ require.NotNil(t, quicConfig)
+ assert.NotSame(t, defaults, quicConfig)
+ assert.Equal(t, 10*time.Second, quicConfig.KeepAlivePeriod)
+ assert.Equal(t, 20*time.Second, quicConfig.MaxIdleTimeout)
+ })
+
+ t.Run("explicit_fields_override_defaults", func(t *testing.T) {
+ quicConfig, err := NewQUICConfig(&global.Http3Config{
+ KeepAlivePeriod: "15s",
+ MaxIdleTimeout: "30s",
+ }, &quic.Config{
+ KeepAlivePeriod: 10 * time.Second,
+ MaxIdleTimeout: 20 * time.Second,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, quicConfig)
+ assert.Equal(t, 15*time.Second, quicConfig.KeepAlivePeriod)
+ assert.Equal(t, 30*time.Second, quicConfig.MaxIdleTimeout)
+ })
+
+ t.Run("explicit_zero_duration_overrides_default", func(t *testing.T) {
+ quicConfig, err := NewQUICConfig(&global.Http3Config{
+ KeepAlivePeriod: "0s",
+ MaxIdleTimeout: "0s",
+ }, &quic.Config{
+ KeepAlivePeriod: 10 * time.Second,
+ MaxIdleTimeout: 20 * time.Second,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, quicConfig)
+ assert.Zero(t, quicConfig.KeepAlivePeriod)
+ assert.Zero(t, quicConfig.MaxIdleTimeout)
+ })
+}
diff --git a/protocol/triple/triple_protocol/server.go
b/protocol/triple/triple_protocol/server.go
index 409b4e485..10cde267a 100644
--- a/protocol/triple/triple_protocol/server.go
+++ b/protocol/triple/triple_protocol/server.go
@@ -43,6 +43,7 @@ import (
"dubbo.apache.org/dubbo-go/v3/common"
"dubbo.apache.org/dubbo-go/v3/common/constant"
"dubbo.apache.org/dubbo-go/v3/global"
+ "dubbo.apache.org/dubbo-go/v3/protocol/triple/internal/http3config"
"dubbo.apache.org/dubbo-go/v3/protocol/triple/openapi"
)
@@ -228,7 +229,7 @@ func (s *Server) startHttp3(tlsConf *tls.Config) error {
http3Config = s.tripleConfig.Http3
}
- quicConfig, err := newQUICConfig(http3Config)
+ quicConfig, err := http3config.NewQUICConfig(http3Config, nil)
if err != nil {
return err
}
@@ -259,7 +260,7 @@ func (s *Server) startHttp2AndHttp3(tlsConf *tls.Config)
error {
http3Config = s.tripleConfig.Http3
}
- quicConfig, err := newQUICConfig(http3Config)
+ quicConfig, err := http3config.NewQUICConfig(http3Config, nil)
if err != nil {
return err
}
diff --git a/protocol/triple/triple_protocol/server_test.go
b/protocol/triple/triple_protocol/server_test.go
index f002682b0..c9ef72dd5 100644
--- a/protocol/triple/triple_protocol/server_test.go
+++ b/protocol/triple/triple_protocol/server_test.go
@@ -23,7 +23,6 @@ import (
"net/http/httptest"
"net/url"
"testing"
- "time"
)
import (
@@ -103,51 +102,6 @@ func TestServer_RegisterMuxHandle(t *testing.T) {
}
}
-func TestNewQUICConfig(t *testing.T) {
- t.Run("defaults_preserved_when_unset", func(t *testing.T) {
- quicConfig, err := newQUICConfig(&global.Http3Config{})
- require.NoError(t, err)
- require.NotNil(t, quicConfig)
- assert.Zero(t, quicConfig.KeepAlivePeriod)
- assert.Zero(t, quicConfig.MaxIdleTimeout)
- assert.Zero(t, quicConfig.MaxIncomingStreams)
- assert.Zero(t, quicConfig.MaxIncomingUniStreams)
- })
-
- t.Run("explicit_fields_are_mapped", func(t *testing.T) {
- quicConfig, err := newQUICConfig(&global.Http3Config{
- KeepAlivePeriod: "15s",
- MaxIdleTimeout: "30s",
- MaxIncomingStreams: 128,
- MaxIncomingUniStreams: 64,
- })
- require.NoError(t, err)
- require.NotNil(t, quicConfig)
- assert.Equal(t, 15*time.Second, quicConfig.KeepAlivePeriod)
- assert.Equal(t, 30*time.Second, quicConfig.MaxIdleTimeout)
- assert.Equal(t, int64(128), quicConfig.MaxIncomingStreams)
- assert.Equal(t, int64(64), quicConfig.MaxIncomingUniStreams)
- })
-
- t.Run("invalid_keep_alive_period_returns_error", func(t *testing.T) {
- quicConfig, err := newQUICConfig(&global.Http3Config{
- KeepAlivePeriod: "invalid",
- })
- require.Error(t, err)
- assert.Nil(t, quicConfig)
- assert.ErrorContains(t, err, "keep-alive-period")
- })
-
- t.Run("invalid_max_idle_timeout_returns_error", func(t *testing.T) {
- quicConfig, err := newQUICConfig(&global.Http3Config{
- MaxIdleTimeout: "invalid",
- })
- require.Error(t, err)
- assert.Nil(t, quicConfig)
- assert.ErrorContains(t, err, "max-idle-timeout")
- })
-}
-
func TestServer_HTTP3PathsUseQUICConfigHelper(t *testing.T) {
t.Run("start_http3_returns_parse_error", func(t *testing.T) {
srv := NewServer("127.0.0.1:0", &global.TripleConfig{