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 9c955e957 test(triple): add concurrent send/close guards for 
duplexHTTPCall (#3462)
9c955e957 is described below

commit 9c955e957050a97ae23d0e29a7fd7b268323ace1
Author: 超級の新人 <[email protected]>
AuthorDate: Tue Jun 30 09:39:04 2026 +0800

    test(triple): add concurrent send/close guards for duplexHTTPCall (#3462)
    
    Investigate whether connect-go v1.19.2's fix for the concurrent Send +
    CloseAndReceive nil-pointer dereference (connectrpc/connect-go#919) applies
    to Dubbo-go's forked Triple runtime.
    
    duplexHTTPCall is not affected: newDuplexHTTPCall allocates the request-body
    io.Pipe at construction and never reassigns requestBodyWriter, so it is 
always
    non-nil and concurrent Write/CloseWrite are safe in any order. The upstream
    race, where the pipe was created lazily on the first Send and a racing
    CloseWrite could leave it nil, is structurally impossible here, so no
    synchronization change is ported.
    
    - add TestDuplexHTTPCallConcurrentWriteAndCloseWrite and
      TestDuplexHTTPCallConcurrentWriteCloseRace as regression guards (run under
      -race) that pin the construction-time pipe invariant
    - document the invariant on requestBodyWriter to keep the allocation out of 
a
      lazy/first-send path
    
    Ref: #3458
---
 .../triple/triple_protocol/duplex_http_call.go     |   4 +
 .../triple_protocol/duplex_http_call_test.go       | 184 +++++++++++++++++++++
 2 files changed, 188 insertions(+)

diff --git a/protocol/triple/triple_protocol/duplex_http_call.go 
b/protocol/triple/triple_protocol/duplex_http_call.go
index ef5fefc33..efaff928e 100644
--- a/protocol/triple/triple_protocol/duplex_http_call.go
+++ b/protocol/triple/triple_protocol/duplex_http_call.go
@@ -38,6 +38,10 @@ type duplexHTTPCall struct {
        // We'll use a pipe as the request body. We hand the read side of the 
pipe to
        // net/http, and we write to the write side (naturally). The two ends 
are
        // safe to use concurrently.
+       //
+       // requestBodyWriter is assigned once here and never reassigned, so it's
+       // always non-nil; don't move this allocation into a lazy/first-send 
path
+       // (see connect-go#919 and duplex_http_call_test.go).
        requestBodyReader *io.PipeReader
        requestBodyWriter *io.PipeWriter
 
diff --git a/protocol/triple/triple_protocol/duplex_http_call_test.go 
b/protocol/triple/triple_protocol/duplex_http_call_test.go
new file mode 100644
index 000000000..f48e22f0c
--- /dev/null
+++ b/protocol/triple/triple_protocol/duplex_http_call_test.go
@@ -0,0 +1,184 @@
+// Copyright 2021-2023 Buf Technologies, Inc.
+//
+// Licensed 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 triple_protocol
+
+import (
+       "io"
+       "net/http"
+       "net/http/httptest"
+       "net/url"
+       "sync"
+       "testing"
+)
+
+import (
+       
"dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol/internal/assert"
+)
+
+// newTestDuplexClientCall builds a duplexHTTPCall for a client-streaming RPC
+// backed by a server that simply drains the request body and replies 200. It 
is
+// the minimal setup needed to exercise the Write/CloseWrite/CloseRead 
lifecycle
+// without pulling in the full codec/stream stack.
+func newTestDuplexClientCall(t *testing.T, client HTTPClient, serverURL 
*url.URL) *duplexHTTPCall {
+       t.Helper()
+       call := newDuplexHTTPCall(
+               t.Context(),
+               client,
+               serverURL,
+               Spec{StreamType: StreamTypeClient, Procedure: 
"/triple.test.v1.RaceService/Sum"},
+               http.Header{},
+       )
+       // makeRequest dereferences validateResponse, so it must be set.
+       call.SetValidateResponse(func(*http.Response) *Error { return nil })
+       return call
+}
+
+// TestDuplexHTTPCallConcurrentWriteAndCloseWrite is a regression GUARD -- not 
a
+// reproducer -- for the concurrent Send + CloseAndReceive nil-pointer
+// dereference fixed upstream in connect-go v1.19.2 
(connectrpc/connect-go#919).
+//
+// Upstream created the request-body io.Pipe lazily, inside the CompareAndSwap
+// winner branch of the first Send. A racing CloseWrite (reached via
+// CloseAndReceive -> CloseRequest -> CloseWrite) could win that CAS first and
+// return without creating the pipe, so a later Send dereferenced a nil
+// requestBodyWriter and crashed at io.(*PipeWriter).Write.
+//
+// Dubbo-go's newDuplexHTTPCall instead allocates the pipe at construction and
+// never reassigns requestBodyWriter, so that ordering is structurally
+// impossible here. Two consequences worth stating plainly:
+//
+//   - On a correct tree this test ALWAYS passes and it cannot reproduce the
+//     panic: with requestBodyWriter set once at construction, concurrent Write
+//     and CloseWrite are safe in either order, so the interleaving below is 
not
+//     load-bearing.
+//   - What actually pins the invariant is 
assert.NotNil(call.requestBodyWriter)
+//     plus the source comment on that field. If pipe creation is moved back 
into
+//     a lazy/first-send path, the assertion fails and the concurrent Write
+//     begins dereferencing nil again -- the regression we want to catch.
+//
+// At this layer Write/CloseWrite/CloseRead are what the Triple client-stream
+// APIs call (grpcClientConn.Send -> marshaler -> Write, CloseRequest ->
+// CloseWrite, CloseResponse -> CloseRead), so it covers the client-stream
+// send + close lifecycle the issue is concerned with.
+func TestDuplexHTTPCallConcurrentWriteAndCloseWrite(t *testing.T) {
+       t.Parallel()
+       handler := http.HandlerFunc(func(w http.ResponseWriter, r 
*http.Request) {
+               _, _ = io.Copy(io.Discard, r.Body)
+               _ = r.Body.Close()
+               w.WriteHeader(http.StatusOK)
+       })
+       server := httptest.NewServer(handler)
+       t.Cleanup(server.Close)
+       serverURL, err := url.Parse(server.URL)
+       assert.Nil(t, err)
+
+       const iterations = 10
+       for range iterations {
+               call := newTestDuplexClientCall(t, server.Client(), serverURL)
+
+               // The invariant that makes the upstream nil-deref impossible 
here: the
+               // write side of the request-body pipe is usable immediately 
after
+               // construction, before any Write or CloseWrite call.
+               assert.NotNil(t, call.requestBodyWriter)
+
+               var (
+                       wg       sync.WaitGroup
+                       panicVal any
+               )
+               wg.Add(1)
+               go func() {
+                       defer wg.Done()
+                       defer func() {
+                               if r := recover(); r != nil {
+                                       panicVal = r
+                               }
+                       }()
+                       _, _ = call.Write([]byte{0, 0, 0, 0, 0})
+               }()
+
+               // Race CloseWrite against the concurrent Write above. The 
order is
+               // intentionally unspecified: both interleavings are safe once
+               // requestBodyWriter is set at construction.
+               _ = call.CloseWrite()
+               wg.Wait()
+
+               if panicVal != nil {
+                       t.Fatalf("concurrent Write during CloseWrite panicked: 
%v", panicVal)
+               }
+               _ = call.CloseRead()
+       }
+}
+
+// TestDuplexHTTPCallConcurrentWriteCloseRace exercises concurrent Write,
+// CloseWrite, CloseRead and the response accessors to guard the concurrent
+// send + close/receive lifecycle (the scenario behind connect-go#919). Like 
the
+// test above this is a guard, not a reproducer: on a correct tree it always
+// passes. It is most valuable under `go test -race`, where it would flag a 
data
+// race on requestBodyWriter or response if the construction-time pipe setup 
were
+// ever broken.
+func TestDuplexHTTPCallConcurrentWriteCloseRace(t *testing.T) {
+       t.Parallel()
+       handler := http.HandlerFunc(func(w http.ResponseWriter, r 
*http.Request) {
+               _, _ = io.Copy(io.Discard, r.Body)
+               _ = r.Body.Close()
+               w.Header().Set("Test-Header", "value")
+               w.WriteHeader(http.StatusOK)
+       })
+       server := httptest.NewServer(handler)
+       t.Cleanup(server.Close)
+       serverURL, err := url.Parse(server.URL)
+       assert.Nil(t, err)
+
+       const (
+               iterations = 30
+               writers    = 4
+       )
+       for range iterations {
+               call := newTestDuplexClientCall(t, server.Client(), serverURL)
+
+               // Same construction-time invariant as the guard test above; 
assert it
+               // here too so a lazy-init regression fails with a clear 
assertion
+               // instead of panicking on a nil writer inside the goroutines 
below.
+               assert.NotNil(t, call.requestBodyWriter)
+
+               var wg sync.WaitGroup
+               for range writers {
+                       wg.Add(1)
+                       go func() {
+                               defer wg.Done()
+                               // After the send side is closed Write returns 
io.EOF rather
+                               // than panicking; any panic here is a real 
regression and is
+                               // allowed to fail the test.
+                               _, _ = call.Write([]byte{0, 0, 0, 0, 0})
+                       }()
+               }
+               wg.Add(1)
+               go func() {
+                       defer wg.Done()
+                       _ = call.CloseWrite()
+               }()
+               wg.Add(1)
+               go func() {
+                       defer wg.Done()
+                       // These block until the response is ready and then 
read the
+                       // response that makeRequest stored; exercises the 
response
+                       // happens-before edge established by closing 
responseReady.
+                       _ = call.ResponseHeader()
+                       _ = call.ResponseTrailer()
+               }()
+               wg.Wait()
+               _ = call.CloseRead()
+       }
+}

Reply via email to