This is an automated email from the ASF dual-hosted git repository.
Alanxtl 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 130b2a686 fix(getty): fix the getty poll bug described at #3509 (#3592)
130b2a686 is described below
commit 130b2a686841f319ee95ff8052e693cfa9731eeb
Author: Xuetao Li <[email protected]>
AuthorDate: Tue Sep 1 17:14:39 2026 +0800
fix(getty): fix the getty poll bug described at #3509 (#3592)
* fix 3509
* import format
* fmt
* fix format
* fix comment
* fix comment
* fix comment
* fix race
---
remoting/getty/getty_client.go | 5 +-
remoting/getty/pool.go | 28 +++--
remoting/getty/pool_test.go | 272 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 297 insertions(+), 8 deletions(-)
diff --git a/remoting/getty/getty_client.go b/remoting/getty/getty_client.go
index ee6432d36..f2b7bec43 100644
--- a/remoting/getty/getty_client.go
+++ b/remoting/getty/getty_client.go
@@ -229,7 +229,7 @@ func (c *Client) RequestContext(ctx context.Context,
request *remoting.Request,
if timeout <= 0 {
timeout = c.opts.RequestTimeout
}
- _, session, err := c.selectSession(c.addr)
+ rpcClient, session, err := c.selectSession(c.addr)
if err != nil {
return perrors.WithStack(err)
}
@@ -256,6 +256,9 @@ func (c *Client) RequestContext(ctx context.Context,
request *remoting.Request,
defer timer.Stop()
select {
case <-timer.C:
+
remoting.RemovePendingResponse(remoting.SequenceType(request.ID))
+ rpcClient.removeSession(session)
+ go session.Close()
return perrors.WithStack(errClientReadTimeout)
case <-response.Done:
err = response.Err
diff --git a/remoting/getty/pool.go b/remoting/getty/pool.go
index 7434e11df..623923172 100644
--- a/remoting/getty/pool.go
+++ b/remoting/getty/pool.go
@@ -19,6 +19,7 @@ package getty
import (
"crypto/tls"
+ "errors"
"fmt"
"math/rand"
"net"
@@ -184,11 +185,16 @@ func (c *gettyRPCClient) selectSession() getty.Session {
if c.sessions == nil {
return nil
}
- count := len(c.sessions)
- if count == 0 {
+ available := make([]getty.Session, 0, len(c.sessions))
+ for _, s := range c.sessions {
+ if s != nil && s.session != nil && !s.session.IsClosed() {
+ available = append(available, s.session)
+ }
+ }
+ if len(available) == 0 {
return nil
}
- return c.sessions[rand.Int31n(int32(count))].session
+ return available[rand.Int31n(int32(len(available)))] // NOSONAR
}
func (c *gettyRPCClient) addSession(session getty.Session) {
@@ -211,6 +217,7 @@ func (c *gettyRPCClient) removeSession(session
getty.Session) {
}
var removeFlag bool
+ var removed bool
func() {
c.lock.Lock()
defer c.lock.Unlock()
@@ -219,12 +226,16 @@ func (c *gettyRPCClient) removeSession(session
getty.Session) {
}
for i, s := range c.sessions {
- if s.session == session {
+ if s != nil && s.session == session {
c.sessions = append(c.sessions[:i],
c.sessions[i+1:]...)
+ removed = true
logger.Debugf("[Remoting][Getty] delete
session=%s index=%d", session.Stat(), i)
break
}
}
+ if !removed {
+ return
+ }
logger.Infof("[Remoting][Getty] after remove session=%s, left
session number=%d", session.Stat(), len(c.sessions))
if len(c.sessions) == 0 {
removeFlag = true
@@ -289,8 +300,9 @@ func (c *gettyRPCClient) isAvailable() bool {
}
func (c *gettyRPCClient) close() error {
- closeErr := perrors.Errorf("close gettyRPCClient{%#v} again", c)
+ firstClose := false
c.once.Do(func() {
+ firstClose = true
var (
gettyClient getty.Client
sessions []*rpcSession
@@ -320,7 +332,9 @@ func (c *gettyRPCClient) close() error {
}
}()
- closeErr = nil
})
- return closeErr
+ if !firstClose {
+ return errors.New("close gettyRPCClient again")
+ }
+ return nil
}
diff --git a/remoting/getty/pool_test.go b/remoting/getty/pool_test.go
index 0559e06d6..a1d8c74ec 100644
--- a/remoting/getty/pool_test.go
+++ b/remoting/getty/pool_test.go
@@ -18,15 +18,26 @@
package getty
import (
+ "net"
"sync"
+ "sync/atomic"
"testing"
+ "time"
)
import (
+ gettylib "github.com/apache/dubbo-getty"
+
+ perrors "github.com/pkg/errors"
+
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
+import (
+ "dubbo.apache.org/dubbo-go/v3/remoting"
+)
+
func TestGettyRPCClientUpdateActive(t *testing.T) {
client := &gettyRPCClient{}
client.updateActive(1234567890)
@@ -132,3 +143,264 @@ func TestGettyRPCClientLifecycle(t *testing.T) {
require.NoError(t, client.close())
assert.Equal(t, int64(0), client.active.Load())
}
+
+type stubSession struct {
+ closed atomic.Bool
+ writes atomic.Int32
+ onWrite func()
+ onClose func()
+ closeOnce sync.Once
+}
+
+func (s *stubSession) ID() uint32 { return 1 }
+func (s *stubSession) SetCompressType(gettylib.CompressType) {}
+func (s *stubSession) LocalAddr() string { return
"127.0.0.1:12345" }
+func (s *stubSession) RemoteAddr() string { return
"127.0.0.1:20880" }
+func (s *stubSession) IncReadPkgNum() {}
+func (s *stubSession) IncWritePkgNum() {}
+func (s *stubSession) UpdateActive() {}
+func (s *stubSession) GetActive() time.Time { return
time.Now() }
+func (s *stubSession) ReadTimeout() time.Duration { return
time.Second }
+func (s *stubSession) SetReadTimeout(time.Duration) {}
+func (s *stubSession) WriteTimeout() time.Duration { return
time.Second }
+func (s *stubSession) SetWriteTimeout(time.Duration) {}
+func (s *stubSession) Send(any) (int, error) { return 0, nil
}
+func (s *stubSession) CloseConn(int) {}
+func (s *stubSession) SetSession(gettylib.Session) {}
+func (s *stubSession) Reset() {}
+func (s *stubSession) Conn() net.Conn { return nil }
+func (s *stubSession) Stat() string { return
"stub-session" }
+func (s *stubSession) IsClosed() bool { return
s.closed.Load() }
+func (s *stubSession) EndPoint() gettylib.EndPoint { return nil }
+func (s *stubSession) SetMaxMsgLen(int) {}
+func (s *stubSession) SetName(string) {}
+func (s *stubSession) SetEventListener(gettylib.EventListener) {}
+func (s *stubSession) SetPkgHandler(gettylib.ReadWriter) {}
+func (s *stubSession) SetReader(gettylib.Reader) {}
+func (s *stubSession) SetWriter(gettylib.Writer) {}
+func (s *stubSession) SetCronPeriod(int) {}
+func (s *stubSession) SetWaitTime(time.Duration) {}
+func (s *stubSession) GetAttribute(any) any { return nil }
+func (s *stubSession) SetAttribute(any, any) {}
+func (s *stubSession) RemoveAttribute(any) {}
+func (s *stubSession) WritePkg(pkg any, timeout time.Duration) (int, int,
error) {
+ s.writes.Add(1)
+ if s.onWrite != nil {
+ s.onWrite()
+ }
+ return 1, 1, nil
+}
+func (s *stubSession) WriteBytes([]byte) (int, error) { return 0, nil }
+func (s *stubSession) WriteBytesArray(...[]byte) (int, error) { return 0, nil }
+func (s *stubSession) Close() {
+ s.closeOnce.Do(func() {
+ s.closed.Store(true)
+ if s.onClose != nil {
+ s.onClose()
+ }
+ })
+}
+
+// This test case verifies the scenario described at
https://github.com/apache/dubbo-go/issues/3509.
+func TestReadTimeoutRemovesHalfDeadSession(t *testing.T) {
+ sess := &stubSession{}
+ client := &Client{addr: "127.0.0.1:20880"}
+ rpcClient := &gettyRPCClient{rpcClient: client, sessions:
[]*rpcSession{{session: sess}}}
+ client.gettyClient = rpcClient
+
+ req := remoting.NewRequest("2.0.2")
+ req.TwoWay = true
+ rsp := remoting.NewPendingResponse(req.ID)
+ remoting.AddPendingResponse(rsp)
+
+ err := client.Request(req, 10*time.Millisecond, rsp)
+ require.Error(t, err)
+ require.ErrorIs(t, err, errClientReadTimeout)
+ assert.Eventually(t, sess.IsClosed, time.Second, time.Millisecond,
"timed out session should be closed")
+ assert.Equal(t, int32(1), sess.writes.Load())
+
+ assert.Nil(t, rpcClient.selectSession())
+ assert.Empty(t, rpcClient.sessions)
+ assert.Nil(t, client.gettyClient, "the connection handle should be
reset after the last session is removed")
+ assert.Nil(t,
remoting.GetPendingResponse(remoting.SequenceType(req.ID)))
+ assert.False(t, client.closed.Load(), "a timed out session must not
close the client")
+}
+
+func TestIssueClosedSessionIsNotSelected(t *testing.T) {
+ sess := &stubSession{}
+ sess.Close()
+ client := &gettyRPCClient{sessions: []*rpcSession{{session: sess}}}
+
+ selected := client.selectSession()
+ assert.Nil(t, selected)
+}
+
+func TestDelayedOnCloseDoesNotResetReplacement(t *testing.T) {
+ client := &Client{addr: "127.0.0.1:20880"}
+ oldSession := &stubSession{}
+ oldPool := &gettyRPCClient{rpcClient: client, sessions:
[]*rpcSession{{session: oldSession}}}
+ client.gettyClient = oldPool
+
+ closeStarted := make(chan struct{})
+ allowOnClose := make(chan struct{})
+ onCloseDone := make(chan struct{})
+ oldSession.onClose = func() {
+ close(closeStarted)
+ <-allowOnClose
+ oldPool.removeSession(oldSession)
+ close(onCloseDone)
+ }
+
+ request := remoting.NewRequest("2.0.2")
+ request.TwoWay = true
+ response := remoting.NewPendingResponse(request.ID)
+ remoting.AddPendingResponse(response)
+
+ err := client.Request(request, 10*time.Millisecond, response)
+ require.ErrorIs(t, err, errClientReadTimeout)
+ <-closeStarted
+
+ replacement := &gettyRPCClient{rpcClient: client, sessions:
[]*rpcSession{{session: &stubSession{}}}}
+ client.gettyClientMux.Lock()
+ client.gettyClient = replacement
+ client.gettyClientMux.Unlock()
+
+ client.resetRpcConn(oldPool)
+ client.gettyClientMux.RLock()
+ assert.Same(t, replacement, client.gettyClient)
+ client.gettyClientMux.RUnlock()
+
+ close(allowOnClose)
+ select {
+ case <-onCloseDone:
+ case <-time.After(time.Second):
+ t.Fatal("timed out waiting for delayed OnClose")
+ }
+
+ client.gettyClientMux.RLock()
+ assert.Same(t, replacement, client.gettyClient)
+ client.gettyClientMux.RUnlock()
+}
+
+func TestTimeoutResetWithConcurrentSelectSession(t *testing.T) {
+ client := &Client{addr: "127.0.0.1:20880"}
+ oldSession := &stubSession{}
+ oldPool := &gettyRPCClient{rpcClient: client, sessions:
[]*rpcSession{{session: oldSession}}}
+ client.gettyClient = oldPool
+
+ closeStarted := make(chan struct{})
+ allowOnClose := make(chan struct{})
+ onCloseDone := make(chan struct{})
+ oldSession.onClose = func() {
+ close(closeStarted)
+ <-allowOnClose
+ oldPool.removeSession(oldSession)
+ close(onCloseDone)
+ }
+
+ request := remoting.NewRequest("2.0.2")
+ request.TwoWay = true
+ response := remoting.NewPendingResponse(request.ID)
+ remoting.AddPendingResponse(response)
+ require.ErrorIs(t, client.Request(request, 10*time.Millisecond,
response), errClientReadTimeout)
+ <-closeStarted
+ client.gettyClientMux.RLock()
+ assert.Nil(t, client.gettyClient)
+ client.gettyClientMux.RUnlock()
+
+ replacementSession := &stubSession{}
+ replacement := &gettyRPCClient{rpcClient: client, sessions:
[]*rpcSession{{session: replacementSession}}}
+ client.gettyClientMux.Lock()
+ client.gettyClient = replacement
+ client.gettyClientMux.Unlock()
+
+ selectDone := make(chan struct{})
+ selectErr := make(chan error, 1)
+ go func() {
+ defer close(selectDone)
+ for range 100 {
+ selectedClient, selectedSession, err :=
client.selectSession(client.addr)
+ if err != nil {
+ selectErr <- err
+ return
+ }
+ if selectedClient != replacement || selectedSession !=
replacementSession {
+ selectErr <- perrors.New("selectSession
returned a stale connection")
+ return
+ }
+ }
+ }()
+
+ close(allowOnClose)
+ select {
+ case err := <-selectErr:
+ t.Fatal(err)
+ case <-selectDone:
+ case <-time.After(time.Second):
+ t.Fatal("timed out waiting for concurrent selectSession")
+ }
+ select {
+ case <-onCloseDone:
+ case <-time.After(time.Second):
+ t.Fatal("timed out waiting for delayed OnClose")
+ }
+}
+
+func TestRequestTimeoutConcurrentWithClose(t *testing.T) {
+ client := &Client{addr: "127.0.0.1:20880"}
+ sess := &stubSession{}
+ rpcClient := &gettyRPCClient{rpcClient: client, sessions:
[]*rpcSession{{session: sess}}}
+ client.gettyClient = rpcClient
+
+ writeStarted := make(chan struct{})
+ sess.onWrite = func() {
+ close(writeStarted)
+ }
+
+ request := remoting.NewRequest("2.0.2")
+ request.TwoWay = true
+ response := remoting.NewPendingResponse(request.ID)
+ remoting.AddPendingResponse(response)
+ requestDone := make(chan error, 1)
+ go func() {
+ requestDone <- client.Request(request, 20*time.Millisecond,
response)
+ }()
+
+ select {
+ case <-writeStarted:
+ case <-time.After(time.Second):
+ t.Fatal("timed out waiting for request write")
+ }
+
+ // Hold the pointer lock before starting Close. Both Close and the
timeout
+ // reset must wait for this lock before accessing gettyClient.
+ client.gettyClientMux.Lock()
+ closeDone := make(chan struct{})
+ go func() {
+ client.Close()
+ close(closeDone)
+ }()
+
+ select {
+ case <-closeDone:
+ t.Fatal("Close must wait for gettyClientMux")
+ case <-time.After(100 * time.Millisecond):
+ }
+ client.gettyClientMux.Unlock()
+
+ select {
+ case err := <-requestDone:
+ require.ErrorIs(t, err, errClientReadTimeout)
+ case <-time.After(time.Second):
+ t.Fatal("timed out waiting for request timeout")
+ }
+ select {
+ case <-closeDone:
+ case <-time.After(time.Second):
+ t.Fatal("timed out waiting for client close")
+ }
+ client.gettyClientMux.RLock()
+ assert.Nil(t, client.gettyClient)
+ client.gettyClientMux.RUnlock()
+ assert.True(t, client.closed.Load())
+}