You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by xi...@apache.org on 2026/03/25 06:35:44 UTC

(tinkerpop) 04/04: added wg to ensure in-flight goroutines complete & added response body drain before close to prevent TCP RST errors

This is an automated email from the ASF dual-hosted git repository.

xiazcy pushed a commit to branch go-http-fix
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit e8ac84d18445642f94b289628715c4a53206942b
Author: Yang Xia <xi...@gmail.com>
AuthorDate: Tue Mar 24 23:34:51 2026 -0700

    added wg to ensure in-flight goroutines complete & added response body drain before close to prevent TCP RST errors
---
 gremlin-go/driver/connection.go      | 13 ++++++++++++-
 gremlin-go/driver/connection_test.go | 24 ++++++++++++++++++++++++
 2 files changed, 36 insertions(+), 1 deletion(-)

diff --git a/gremlin-go/driver/connection.go b/gremlin-go/driver/connection.go
index 5396b6ab43..54def5821a 100644
--- a/gremlin-go/driver/connection.go
+++ b/gremlin-go/driver/connection.go
@@ -29,6 +29,7 @@ import (
 	"net"
 	"net/http"
 	"strings"
+	"sync"
 	"time"
 )
 
@@ -53,6 +54,7 @@ type connection struct {
 	logHandler   *logHandler
 	serializer   *GraphBinarySerializer
 	interceptors []RequestInterceptor
+	wg           sync.WaitGroup
 }
 
 // Connection pool defaults aligned with Java driver
@@ -121,7 +123,11 @@ func (c *connection) AddInterceptor(interceptor RequestInterceptor) {
 func (c *connection) submit(req *RequestMessage) (ResultSet, error) {
 	rs := newChannelResultSet()
 
-	go c.executeAndStream(req, rs)
+	c.wg.Add(1)
+	go func() {
+		defer c.wg.Done()
+		c.executeAndStream(req, rs)
+	}()
 
 	return rs, nil
 }
@@ -206,6 +212,10 @@ func (c *connection) executeAndStream(req *RequestMessage, rs ResultSet) {
 		return
 	}
 	defer func() {
+		// Drain any unread bytes so the connection can be reused gracefully.
+		// Without this, Go's HTTP client sends a TCP RST instead of FIN,
+		// causing "Connection reset by peer" errors on the server.
+		io.Copy(io.Discard, resp.Body)
 		if err := resp.Body.Close(); err != nil {
 			c.logHandler.logf(Debug, failedToCloseResponseBody, err.Error())
 		}
@@ -328,5 +338,6 @@ func tryExtractJSONError(body string) string {
 }
 
 func (c *connection) close() {
+	c.wg.Wait()
 	c.httpClient.CloseIdleConnections()
 }
diff --git a/gremlin-go/driver/connection_test.go b/gremlin-go/driver/connection_test.go
index 980040be5d..7d95192211 100644
--- a/gremlin-go/driver/connection_test.go
+++ b/gremlin-go/driver/connection_test.go
@@ -1093,6 +1093,30 @@ func TestConnectionWithMockServer(t *testing.T) {
 		assert.Equal(t, graphBinaryMimeType, interceptorHeaders.Get("Content-Type"))
 		assert.Equal(t, graphBinaryMimeType, interceptorHeaders.Get("Accept"))
 	})
+
+	t.Run("close waits for in-flight requests", func(t *testing.T) {
+		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			time.Sleep(200 * time.Millisecond)
+			w.WriteHeader(http.StatusOK)
+		}))
+		defer server.Close()
+
+		conn := newConnection(newTestLogHandler(), server.URL, &connectionSettings{})
+
+		rs, err := conn.submit(&RequestMessage{Gremlin: "g.V()", Fields: map[string]interface{}{}})
+		require.NoError(t, err)
+
+		start := time.Now()
+		conn.close()
+		elapsed := time.Since(start)
+
+		// close() should have waited for the in-flight goroutine
+		assert.GreaterOrEqual(t, elapsed.Milliseconds(), int64(150),
+			"close() should wait for in-flight requests to complete")
+
+		// ResultSet should be closed (goroutine finished)
+		_, _ = rs.All()
+	})
 }
 
 // Tests for connection pool configuration settings